Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Invoice">
    <div class="invoice-container">
      <!-- Header Section -->
      <div class="invoice-header">
        <h1 class="invoice-title">Invoice</h1>
        <div class="invoice-date">{{ invoiceData.createdAt ? formatDate(invoiceData.createdAt) : formattedDate }}</div>
      </div>

      <!-- Empty State -->
      <div v-if="invoiceData.lineItems.length === 0" class="empty-state">
        <p>No services selected</p>
        <ion-button @click="router.push('/cart')">Back to Cart</ion-button>
      </div>

      <template v-else>
        <!-- Copy All Button -->
        <div class="copy-all-row">
          <ion-button size="small" fill="outline" @click="copyAllToClipboard">
            <ion-icon :icon="copyOutline" slot="start"></ion-icon>
            Copy All
          </ion-button>
        </div>

        <!-- Company Information Section -->
        <div class="invoice-section">
          <div class="section-header">
            <h2 class="section-title">Company Information</h2>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySection('company')"></ion-icon>
          </div>
          <div class="section-content">
            <p class="company-name">{{ organizationName }}</p>
            <p class="company-detail">404 McGhee Drive, Dalton, GA 30721, United States</p>
            <p class="company-detail">706-259-8892</p>
            <p class="company-detail">info@thenewflatrate.com</p>
          </div>
        </div>

        <!-- Customer Information Section -->
        <div class="invoice-section">
          <div class="section-header">
            <h2 class="section-title">Customer Information</h2>
            <div class="section-icons">
              <ion-icon :icon="pencilOutline" class="edit-icon" :class="{ active: editingCustomer }" @click="editingCustomer = !editingCustomer"></ion-icon>
              <ion-icon :icon="copyOutline" class="copy-icon" @click="copySection('customer')"></ion-icon>
            </div>
          </div>
          <!-- Read-only view -->
          <div v-if="!editingCustomer" class="section-content">
            <template v-if="tnfrStore.invoice.customerName || tnfrStore.invoice.customerAddress || tnfrStore.invoice.customerPhone || tnfrStore.invoice.customerEmail">
              <p v-if="tnfrStore.invoice.customerName" class="company-name">{{ tnfrStore.invoice.customerName }}</p>
              <p v-if="tnfrStore.invoice.customerAddress" class="company-detail">{{ tnfrStore.invoice.customerAddress }}</p>
              <p v-if="tnfrStore.invoice.customerPhone" class="company-detail">{{ tnfrStore.invoice.customerPhone }}</p>
              <p v-if="tnfrStore.invoice.customerEmail" class="company-detail">{{ tnfrStore.invoice.customerEmail }}</p>
            </template>
            <p v-else class="empty-customer">No customer information added. Tap <ion-icon :icon="pencilOutline" class="inline-icon"></ion-icon> to add.</p>
          </div>
          <!-- Editable view -->
          <div v-else class="section-content customer-form">
            <ion-input
              v-model="tnfrStore.invoice.customerName"
              placeholder="Customer Name (optional)"
              class="customer-input"
            ></ion-input>
            <ion-input
              v-model="tnfrStore.invoice.customerAddress"
              placeholder="Address (optional)"
              class="customer-input"
            ></ion-input>
            <ion-input
              v-model="tnfrStore.invoice.customerPhone"
              placeholder="Phone (optional)"
              class="customer-input"
            ></ion-input>
            <ion-input
              v-model="tnfrStore.invoice.customerEmail"
              placeholder="Email (optional)"
              class="customer-input"
            ></ion-input>
          </div>
        </div>

        <!-- Line Items Section -->
        <div class="invoice-section">
          <div class="section-header">
            <h2 class="section-title">Services Provided</h2>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySection('services')"></ion-icon>
          </div>
          <div class="section-content">
            <div
              v-for="(item, index) in invoiceData.lineItems"
              :key="item.jobId"
              class="line-item"
            >
              <div class="line-item-header">
                <div class="line-item-details">
                  <h3 class="line-item-title">{{ item.offerRefId }} {{ item.menuName }}</h3>
                  <div class="line-item-meta">
                    <span class="line-item-tier">{{ item.tierName }}</span>
                  </div>
                </div>
                <div class="line-item-price">
                  <template v-if="invoiceData.applyDiscount && item.discountPrice">
                    <div class="price-discount">
                      <show-currency :currencyIn="item.discountPrice" />
                    </div>
                    <div class="price-original">
                      <show-currency :currencyIn="item.price" />
                    </div>
                  </template>
                  <template v-else>
                    <show-currency :currencyIn="item.price" />
                  </template>
                </div>
              </div>
              <!-- Tier Content -->
              <ul v-if="item.tierContent && item.tierContent.length > 0" class="tier-content-list">
                <li v-for="(content, contentIndex) in item.tierContent" :key="contentIndex" class="tier-content-item">
                  {{ content }}
                </li>
              </ul>
            </div>
          </div>
        </div>

        <!-- Total Section -->
        <div class="invoice-section total-section">
          <div class="section-header">
            <h2 class="section-title">Total</h2>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySection('total')"></ion-icon>
          </div>
          <div class="section-content">
            <!-- Show subtotal and discount if service agreement applied -->
            <template v-if="invoiceData.applyDiscount">
              <div class="total-row subtotal-row">
                <span class="total-label">Subtotal:</span>
                <span class="total-value subtotal-value">
                  <show-currency :currencyIn="invoiceData.subtotal" />
                </span>
              </div>
              <div class="total-row discount-row">
                <span class="total-label">Service Agreement Discount:</span>
                <span class="total-value discount-value">
                  -<show-currency :currencyIn="invoiceData.subtotal - invoiceData.discountSubtotal" />
                </span>
              </div>
            </template>

            <div class="total-row final-total-row">
              <span class="total-label">Total:</span>
              <span class="total-value final-total-value">
                <show-currency :currencyIn="invoiceData.total" />
              </span>
            </div>

            <!-- Payment Plan -->
            <template v-if="invoiceData.showPlan && invoiceData.monthlyPayment">
              <div class="payment-plan-row">
                <span class="payment-plan-label">Payment Plan:</span>
                <span class="payment-plan-value">
                  <show-currency :currencyIn="invoiceData.monthlyPayment" />/mo
                  <span class="payment-plan-terms">× {{ invoiceData.numberOfPayments }} payments</span>
                </span>
              </div>
            </template>
          </div>
        </div>

        <!-- Action Buttons -->
        <div class="invoice-actions">
          <ion-button
            expand="block"
            color="success"
            size="large"
            @click="showPaymentModal = true"
          >
            {{ tnfrStore.invoice.paymentConfirmed ? 'Update Payment Info' : 'Confirm Payment' }}
          </ion-button>
        </div>
      </template>
    </div>

    <!-- Payment Confirmation Modal -->
    <ion-modal :is-open="showPaymentModal" @didDismiss="showPaymentModal = false">
      <ion-header>
        <ion-toolbar>
          <ion-title>Payment Information</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="showPaymentModal = false">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div class="payment-modal-content">
          <ion-textarea
            v-model="paymentInfo"
            :rows="4"
            placeholder="Enter payment details (optional)..."
            class="payment-info-textarea"
          ></ion-textarea>
          <ion-button
            expand="block"
            color="success"
            size="large"
            @click="confirmPayment"
          >
            Confirm Payment
          </ion-button>
        </div>
      </ion-content>
    </ion-modal>
  </BaseLayout>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import {
  IonButton,
  IonIcon,
  IonModal,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonButtons,
  IonContent,
  IonTextarea,
  IonInput,
  toastController,
} from '@ionic/vue';
import { copyOutline } from 'ionicons/icons';
import { mdPencil as pencilOutline } from '@/icons/customIcons';
import BaseLayout from '@/components/BaseLayout.vue';
import ShowCurrency from '@/components/ShowCurrency.vue';
import { useTnfrStore } from '@/stores/tnfr';
import { getApi } from '@/dataAccess/getApi';

const router = useRouter();
const tnfrStore = useTnfrStore();
const showPaymentModal = ref(false);
const organizationName = ref('Company Name');
const paymentInfo = ref('');

// Customer information editing state
const editingCustomer = ref(false);

// Get invoice data from store
const invoiceData = computed(() => tnfrStore.createInvoice());

// Get current date formatted
const formattedDate = computed(() => {
  const date = new Date();
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
});

// Format date from ISO string
const formatDate = (isoDate: string) => {
  const date = new Date(isoDate);
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};

// Copy all invoice content to clipboard
const copyAllToClipboard = async () => {
  const invoiceText = buildInvoiceText();
  try {
    await navigator.clipboard.writeText(invoiceText);
    const toast = await toastController.create({
      message: 'Invoice copied to clipboard',
      duration: 2000,
      color: 'success',
      position: 'bottom',
    });
    await toast.present();
  } catch (err) {
    console.error('[Invoice] Failed to copy to clipboard:', err);
  }
};

// Build full invoice text
const buildInvoiceText = (): string => {
  const data = invoiceData.value;
  let text = '=== SERVICE INVOICE ===\n\n';

  // Date
  text += `Date: ${formattedDate.value}\n\n`;

  // Company Information
  text += '--- Company Information ---\n';
  text += `${organizationName.value}\n`;
  text += '404 McGhee Drive, Dalton, GA 30721, United States\n';
  text += '706-259-8892\n';
  text += 'info@thenewflatrate.com\n';
  text += '\n';

  // Customer Information
  if (tnfrStore.invoice.customerName || tnfrStore.invoice.customerAddress || tnfrStore.invoice.customerPhone || tnfrStore.invoice.customerEmail) {
    text += '--- Customer Information ---\n';
    if (tnfrStore.invoice.customerName) text += `${tnfrStore.invoice.customerName}\n`;
    if (tnfrStore.invoice.customerAddress) text += `${tnfrStore.invoice.customerAddress}\n`;
    if (tnfrStore.invoice.customerPhone) text += `${tnfrStore.invoice.customerPhone}\n`;
    if (tnfrStore.invoice.customerEmail) text += `${tnfrStore.invoice.customerEmail}\n`;
    text += '\n';
  }

  // Services
  text += '--- Services Provided ---\n';
  data.lineItems.forEach((item, index) => {
    text += `\n${index + 1}. ${item.offerRefId} ${item.menuName}\n`;
    text += `   Tier: ${item.tierName}\n`;
    if (item.tierContent && item.tierContent.length > 0) {
      item.tierContent.forEach(content => {
        text += `   • ${content}\n`;
      });
    }
    if (data.applyDiscount && item.discountPrice) {
      text += `   Price: $${item.discountPrice.toFixed(2)} (was $${item.price.toFixed(2)})\n`;
    } else {
      text += `   Price: $${item.price.toFixed(2)}\n`;
    }
  });
  text += '\n';

  // Total
  text += '--- Total ---\n';
  if (data.applyDiscount) {
    text += `Subtotal: $${data.subtotal.toFixed(2)}\n`;
    text += `Discount: -$${(data.subtotal - data.discountSubtotal).toFixed(2)}\n`;
  }
  text += `Total: $${data.total.toFixed(2)}\n`;

  if (data.showPlan && data.monthlyPayment) {
    text += `\nPayment Plan: $${data.monthlyPayment.toFixed(2)}/mo × ${data.numberOfPayments} payments\n`;
  }

  return text;
};

// Copy specific section to clipboard
const copySection = async (section: 'company' | 'customer' | 'services' | 'total') => {
  const data = invoiceData.value;
  let text = '';

  if (section === 'company') {
    text = 'Company Information:\n';
    text += `${organizationName.value}\n`;
    text += '404 McGhee Drive, Dalton, GA 30721, United States\n';
    text += '706-259-8892\n';
    text += 'info@thenewflatrate.com\n';
  } else if (section === 'customer') {
    text = 'Customer Information:\n';
    if (tnfrStore.invoice.customerName) text += `${tnfrStore.invoice.customerName}\n`;
    if (tnfrStore.invoice.customerAddress) text += `${tnfrStore.invoice.customerAddress}\n`;
    if (tnfrStore.invoice.customerPhone) text += `${tnfrStore.invoice.customerPhone}\n`;
    if (tnfrStore.invoice.customerEmail) text += `${tnfrStore.invoice.customerEmail}\n`;
    if (!tnfrStore.invoice.customerName && !tnfrStore.invoice.customerAddress && !tnfrStore.invoice.customerPhone && !tnfrStore.invoice.customerEmail) {
      text += '(No customer information provided)\n';
    }
  } else if (section === 'services') {
    text = 'Services Provided:\n\n';
    data.lineItems.forEach((item, index) => {
      text += `${index + 1}. ${item.offerRefId} ${item.menuName}\n`;
      text += `   Tier: ${item.tierName}\n`;
      if (item.tierContent && item.tierContent.length > 0) {
        item.tierContent.forEach(content => {
          text += `   • ${content}\n`;
        });
      }
      if (data.applyDiscount && item.discountPrice) {
        text += `   Price: $${item.discountPrice.toFixed(2)} (was $${item.price.toFixed(2)})\n`;
      } else {
        text += `   Price: $${item.price.toFixed(2)}\n`;
      }
      text += '\n';
    });
  } else if (section === 'total') {
    if (data.applyDiscount) {
      text += `Subtotal: $${data.subtotal.toFixed(2)}\n`;
      text += `Discount: -$${(data.subtotal - data.discountSubtotal).toFixed(2)}\n`;
    }
    text = `Total: $${data.total.toFixed(2)}`;
    if (data.showPlan && data.monthlyPayment) {
      text += `\nPayment Plan: $${data.monthlyPayment.toFixed(2)}/mo × ${data.numberOfPayments} payments`;
    }
  }

  try {
    await navigator.clipboard.writeText(text);
    const toast = await toastController.create({
      message: `${section.charAt(0).toUpperCase() + section.slice(1)} copied`,
      duration: 1500,
      color: 'success',
      position: 'bottom',
    });
    await toast.present();
  } catch (err) {
    console.error('[Invoice] Failed to copy to clipboard:', err);
  }
};

// Confirm payment handler
const confirmPayment = async () => {
  tnfrStore.invoice.paymentConfirmed = true;
  tnfrStore.invoice.paymentInfo = paymentInfo.value;
  await tnfrStore.save();

  showPaymentModal.value = false;

  router.push('/review');
};

onMounted(async () => {
  await tnfrStore.load();

  // Pre-populate payment info from store
  if (tnfrStore.invoice.paymentInfo) {
    paymentInfo.value = tnfrStore.invoice.paymentInfo;
  }

  // Fetch organization name
  try {
    const { pb } = await getApi();
    const orgId = pb.authStore.record?.activeOrg;
    if (orgId) {
      const org = await pb.collection('organizations').getOne(orgId);
      if (org?.name) {
        organizationName.value = org.name;
      }
    }
  } catch (error) {
    console.error('[Invoice] Failed to load organization name:', error);
  }
});
</script>

<style scoped>
.invoice-container {
  max-width: 900px;
  margin: 0 auto;
  padding: 40px 20px;
}

.invoice-header {
  text-align: center;
  margin-bottom: 40px;
}

.copy-all-row {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 12px;
}

.invoice-title {
  margin: 0 0 16px 0;
  color: var(--ion-text-color);
  font-size: 42px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1px;
}

.invoice-date {
  color: var(--ion-color-medium);
  font-size: 18px;
  font-weight: 600;
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 300px;
  gap: 16px;
}

.empty-state p {
  font-size: 18px;
  color: var(--ion-color-medium);
}

.invoice-section {
  background-color: var(--ion-background-color-step-50);
  border-radius: 12px;
  padding: 24px;
  margin-bottom: 24px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 12px;
  border-bottom: 2px solid var(--ion-color-primary);
}

.section-title {
  margin: 0;
  color: var(--ion-text-color);
  font-size: 24px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.section-icons {
  display: flex;
  gap: 16px;
  align-items: center;
}

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

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

.edit-icon.active {
  color: var(--ion-color-success);
}

.empty-customer {
  color: var(--ion-color-medium);
  font-style: italic;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 4px;
}

.inline-icon {
  font-size: 16px;
  vertical-align: middle;
}

.section-content {
  color: var(--ion-text-color);
}

.company-name {
  margin: 0 0 12px 0;
  font-size: 22px;
  font-weight: 700;
  color: var(--ion-text-color);
}

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

.customer-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.customer-input {
  --background: var(--ion-background-color);
  --color: var(--ion-text-color);
  --placeholder-color: var(--ion-color-medium);
  --padding-start: 16px;
  --padding-end: 16px;
  --padding-top: 12px;
  --padding-bottom: 12px;
  border: 1px solid var(--ion-color-light-shade);
  border-radius: 8px;
  font-size: 16px;
}

.customer-input:focus-within {
  border-color: var(--ion-color-primary);
}

.line-item {
  padding: 16px 0;
  border-bottom: 1px solid var(--ion-color-light-shade);
}

.line-item:last-child {
  border-bottom: none;
}

.tier-content-list {
  margin: 12px 0 0 0;
  padding: 0;
  list-style: none;
}

.tier-content-item {
  position: relative;
  padding-left: 16px;
  margin-bottom: 6px;
  font-size: 14px;
  color: var(--ion-color-medium-shade);
  line-height: 1.4;
}

.tier-content-item::before {
  content: "•";
  position: absolute;
  left: 0;
  color: var(--ion-color-primary);
}

.line-item-header {
  display: flex;
  gap: 16px;
  align-items: flex-start;
}

.line-item-details {
  flex: 1;
}

.line-item-title {
  margin: 0 0 8px 0;
  font-size: 20px;
  font-weight: 700;
  color: var(--ion-text-color);
}

.line-item-meta {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
}

.line-item-tier {
  display: inline-block;
  padding: 4px 12px;
  background-color: var(--ion-color-primary);
  color: var(--ion-color-primary-contrast);
  border-radius: 6px;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
}

.line-item-price {
  font-size: 28px;
  font-weight: 700;
  color: var(--ion-text-color);
  white-space: nowrap;
  text-align: right;
}

.price-discount {
  font-size: 28px;
  font-weight: 700;
  color: var(--ion-text-color);
}

.price-original {
  font-size: 18px;
  color: var(--ion-color-medium);
  text-decoration: line-through;
}

.total-section {
  background-color: var(--ion-background-color);
  border: 1px solid var(--ion-color-medium);
}

.total-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
}

.subtotal-row {
  border-bottom: 1px solid var(--ion-color-light-shade);
}

.discount-row {
  border-bottom: 1px solid var(--ion-color-light-shade);
}

.discount-row .total-value {
  color: var(--ion-text-color);
}

.final-total-row {
  padding-top: 16px;
}

.total-label {
  font-size: 20px;
  font-weight: 600;
  color: var(--ion-text-color);
}

.final-total-row .total-label {
  font-size: 28px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1px;
}

.total-value {
  font-size: 24px;
  font-weight: 700;
  color: var(--ion-text-color);
}

.final-total-value {
  font-size: 36px;
  color: var(--ion-text-color);
}

.payment-plan-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 2px dashed var(--ion-color-medium);
}

.payment-plan-label {
  font-size: 18px;
  font-weight: 600;
  color: var(--ion-color-medium);
}

.payment-plan-value {
  font-size: 24px;
  font-weight: 700;
  color: var(--ion-color-primary);
}

.payment-plan-terms {
  font-size: 14px;
  font-weight: 500;
  color: var(--ion-color-medium);
  margin-left: 8px;
}

.invoice-actions {
  margin-top: 40px;
  max-width: 400px;
  margin-left: auto;
  margin-right: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.invoice-actions ion-button {
  --padding-top: 16px;
  --padding-bottom: 16px;
  font-weight: 600;
  font-size: 16px;
}

/* Payment Modal Styles */
.payment-modal-content {
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding: 20px;
}

.payment-info-textarea {
  --background: var(--ion-color-light);
  --color: var(--ion-text-color);
  --padding-start: 16px;
  --padding-end: 16px;
  --padding-top: 12px;
  --padding-bottom: 12px;
  border: 1px solid var(--ion-color-medium);
  border-radius: 8px;
  font-size: 16px;
  line-height: 1.5;
}

.payment-modal-content ion-button {
  --padding-top: 20px;
  --padding-bottom: 20px;
  font-weight: 600;
  font-size: 18px;
}

/* Mobile adjustments */
@media (max-width: 767px) {
  .invoice-container {
    padding: 20px 16px;
  }

  .invoice-title {
    font-size: 32px;
  }

  .invoice-date {
    font-size: 16px;
  }

  .section-title {
    font-size: 20px;
  }

  .line-item-header {
    flex-wrap: wrap;
  }

  .line-item-price {
    width: 100%;
    margin-top: 12px;
    text-align: left;
  }

  .price-discount {
    font-size: 24px;
  }

  .total-label {
    font-size: 18px;
  }

  .final-total-row .total-label {
    font-size: 22px;
  }

  .total-value {
    font-size: 20px;
  }

  .final-total-value {
    font-size: 28px;
  }

  .payment-plan-value {
    font-size: 20px;
  }
}
</style>