Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Price List">
    <div class="price-list-container">
      <h1 class="page-title">Price List</h1>
      <div class="content-section">
        <div v-if="loading" class="loading-state">
          <ion-spinner name="crescent"></ion-spinner>
          <p>Loading price list...</p>
        </div>
        <div v-else-if="offers.length === 0 && !testMode" class="empty-state">
          <p>No offers found.</p>
        </div>
        <div v-else-if="!testMode" class="formula-info">
          To learn more about the pricing formula and variables, go to <router-link to="/framework">/framework</router-link> and click "Calculate Price"
        </div>
        <div v-if="!loading && offers.length > 0 && !testMode" class="results-info">
          Showing {{ offers.length }} of {{ offersTotal }} offers
          <button v-if="offers.length < offersTotal" class="show-all-btn" @click="showAllOffers">Show All</button>
        </div>
        <div v-if="!loading && (offers.length > 0 || testMode)" class="formula-params">
          <div class="param-item">
            <span class="param-label">Hourly Fee</span>
            <span class="param-value">{{ formulaParams.hourlyFeeConverted }}</span>
          </div>
          <div class="param-item">
            <span class="param-label">Sales Tax</span>
            <span class="param-value">{{ formulaParams.salesTax }}</span>
          </div>
          <div class="param-item">
            <span class="param-label">Service Call Fee</span>
            <span class="param-value">{{ formulaParams.serviceCallFeeConverted }}</span>
          </div>
          <div class="param-item">
            <span class="param-label">Extra Hours</span>
            <input
              v-model.number="extraHours"
              type="number"
              step="0.5"
              min="0"
              class="param-input"
            />
          </div>
          <div class="param-item">
            <span class="param-label">Extra Hr Discount</span>
            <input
              v-model.number="extraHoursDiscount"
              type="number"
              step="0.1"
              min="0"
              max="1"
              class="param-input"
            />
          </div>
          <label class="toggle-label">
            <input type="checkbox" v-model="useTierMultiplier" />
            Use Tier Multiplier
          </label>
          <button v-if="!testMode" class="recalculate-btn" @click="reloadPriceList">Recalculate</button>
          <button v-if="!csvFileName" class="test-btn" @click="triggerCsvUpload">Test CSV</button>
          <input
            ref="csvInput"
            type="file"
            accept=".csv"
            style="display: none"
            @change="handleCsvUpload"
          />
        </div>
        <div v-if="csvFileName" class="csv-file-banner">
          <span class="csv-file-name">{{ csvFileName }}</span>
          <button v-if="!testMode" class="parse-btn" @click="parseCsvFile">Parse</button>
          <button class="clear-btn" @click="clearCsvFile">Clear</button>
          <span v-if="testMode" class="csv-status">{{ testOffers.length }} items loaded</span>
        </div>
        <ion-grid v-if="displayOffers.length > 0" class="data-table">
          <ion-row class="table-header">
            <ion-col size="2">Name</ion-col>
            <ion-col size="1.5">Time</ion-col>
            <ion-col size="1.5" class="text-right">Mat Fee</ion-col>
            <ion-col size="1.5" class="text-right">Time Fee</ion-col>
            <ion-col size="1.5" class="text-right">All Fees</ion-col>
            <ion-col size="1.5" class="text-right">Imp Rate</ion-col>
            <ion-col size="1.25" class="text-right">Extra</ion-col>
            <ion-col size="1.25" class="text-right">Price</ion-col>
          </ion-row>
          <ion-row
            v-for="offer in displayOffers"
            :key="offer.id"
            class="table-row"
            :class="{ clickable: !testMode }"
            @click="!testMode && openModal(offer)"
          >
            <ion-col size="2">
              <div class="cell-primary">{{ offer.name || offer.refId || offer.id }}</div>
              <div class="cell-secondary">
                <span v-if="offer.tierMultiplier != null">
                  Band-Aid × {{ offer.tierMultiplier.toFixed(2) }}
                </span>
              </div>
            </ion-col>
            <template v-if="!offer.isBandAid && useTierMultiplier && extraHours > 0">
              <ion-col size="1.5"><div class="cell-primary">---</div></ion-col>
              <ion-col size="1.5" class="text-right"><div class="cell-primary">---</div></ion-col>
              <ion-col size="1.5" class="text-right"><div class="cell-primary">---</div></ion-col>
              <ion-col size="1.5" class="text-right"><div class="cell-primary">---</div></ion-col>
              <ion-col size="1.5" class="text-right"><div class="cell-primary">---</div></ion-col>
              <ion-col size="1.25" class="text-right"><div class="cell-primary">---</div></ion-col>
              <ion-col size="1.25" class="text-right">
                <div class="cell-primary">{{ offer.derivedFinalPrice || '—' }}</div>
              </ion-col>
            </template>
            <template v-else>
              <ion-col size="1.5">
                <div class="cell-primary">{{ offer.totalHours }} hrs</div>
                <div class="cell-secondary">
                  <span v-for="(item, idx) in (offer.costsTime || [])" :key="item.id" class="ref-tag">
                    {{ item.refId || item.name }}<span v-if="idx < (offer.costsTime?.length || 0) - 1">, </span>
                  </span>
                </div>
              </ion-col>
              <ion-col size="1.5" class="text-right">
                <div class="cell-primary">{{ offer.derivedVarsConverted?.materialFee?.formatted || '—' }}</div>
                <div class="cell-secondary">
                  <span v-for="(item, idx) in (offer.costsMaterial || [])" :key="item.id" class="ref-tag">
                    {{ item.refId || item.name }}<span v-if="idx < (offer.costsMaterial?.length || 0) - 1">, </span>
                  </span>
                </div>
              </ion-col>
              <ion-col size="1.5" class="text-right">
                <div class="cell-primary">{{ offer.derivedVarsConverted?.timeFee?.formatted || '—' }}</div>
              </ion-col>
              <ion-col size="1.5" class="text-right">
                <div class="cell-primary">{{ offer.derivedVarsConverted?.tierPrice?.formatted || '—' }}</div>
              </ion-col>
              <ion-col size="1.5" class="text-right">
                <div class="cell-primary">{{ offer.derivedVarsConverted?.impliedHourlyRate?.formatted || '—' }}</div>
              </ion-col>
              <ion-col size="1.25" class="text-right">
                <div class="cell-primary">{{ extraHours }} hrs</div>
              </ion-col>
              <ion-col size="1.25" class="text-right">
                <div class="cell-primary">{{ offer.finalPriceConverted?.formatted || '—' }}</div>
              </ion-col>
            </template>
          </ion-row>
        </ion-grid>
      </div>
    </div>

    <!-- Detail Modal -->
    <ion-modal :is-open="isModalOpen" @didDismiss="closeModal">
      <ion-header>
        <ion-toolbar>
          <ion-title>{{ selectedOffer?.name || selectedOffer?.refId || 'Offer Details' }}</ion-title>
          <ion-buttons slot="end">
            <ion-button v-if="pendingChanges.length > 0" @click="showChangesetPreview = !showChangesetPreview">
              {{ showChangesetPreview ? 'Edit' : 'Save' }}
            </ion-button>
            <ion-button @click="closeModal">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <!-- Changeset Preview -->
        <div v-if="showChangesetPreview" class="changeset-preview">
          <div class="commit-controls">
            <select v-model="selectedBookId" class="book-select">
              <option value="">-- Select book --</option>
              <option v-for="book in availableBooks" :key="book.id" :value="book.id">
                {{ book.refId || book.name || book.id }}
              </option>
            </select>
            <ion-button
              :disabled="!selectedBookId || pendingChanges.length === 0 || isCommitting"
              @click="commitPendingChanges"
            >
              {{ isCommitting ? 'Committing...' : 'Commit' }}
            </ion-button>
          </div>
          <div v-if="commitStatus" :class="['commit-status', commitStatus.success ? 'success' : 'error']">
            {{ commitStatus.message }}
          </div>
          <h3>Changeset Preview</h3>
          <pre class="changeset-json">{{ JSON.stringify(pendingChanges, null, 2) }}</pre>
        </div>
        <div v-else-if="selectedOffer" class="modal-content">
          <!-- Name Section -->
          <div class="detail-section">
            <div class="detail-header">
              <h3>Name</h3>
              <ion-icon
                :icon="isEditing.name ? checkmarkOutline : pencilOutline"
                class="edit-icon"
                @click="toggleEdit('name')"
              ></ion-icon>
            </div>
            <div v-if="isEditing.name" class="edit-field">
              <ion-input v-model="editValues.name" fill="outline"></ion-input>
            </div>
            <div v-else class="detail-value">{{ selectedOffer.name || selectedOffer.refId || selectedOffer.id }}</div>
          </div>

          <!-- Time Costs Section -->
          <div class="detail-section">
            <div class="detail-header">
              <h3>Time Costs</h3>
              <ion-icon
                :icon="isEditing.costsTime ? checkmarkOutline : pencilOutline"
                class="edit-icon"
                @click="toggleEdit('costsTime')"
              ></ion-icon>
            </div>
            <div class="detail-summary">Total: {{ selectedOffer.totalHours }} hrs</div>
            <div v-if="isEditing.costsTime" class="edit-list">
              <div v-for="(item, idx) in editValues.costsTime" :key="idx" class="edit-list-item">
                <ion-input v-model="item.refId" placeholder="RefId" fill="outline" class="edit-input-small"></ion-input>
                <ion-input v-model.number="item.hours" type="number" placeholder="Hours" fill="outline" class="edit-input-small"></ion-input>
              </div>
              <!-- Add new time cost -->
              <div v-if="!showAddTimeCost" class="add-item-trigger" @click="showAddTimeCost = true">
                <ion-icon :icon="addCircleOutline" class="add-icon"></ion-icon>
              </div>
              <div v-else class="add-item-form">
                <ion-input v-model="newTimeCost.refId" placeholder="RefId" fill="outline" class="edit-input-small"></ion-input>
                <ion-input v-model.number="newTimeCost.hours" type="number" step="0.5" placeholder="Hours" fill="outline" class="edit-input-small"></ion-input>
                <ion-button size="small" @click="addTimeCost">Add</ion-button>
                <ion-button size="small" fill="clear" color="medium" @click="cancelAddTimeCost">Cancel</ion-button>
              </div>
            </div>
            <div v-else class="detail-list">
              <div v-for="item in (selectedOffer.costsTime || [])" :key="item.id" class="detail-list-item">
                <span class="item-ref">{{ item.refId || item.name }}</span>
                <span class="item-value">{{ item.hours }} hrs</span>
              </div>
              <div v-if="(selectedOffer.costsTime?.length || 0) === 0" class="empty-list">No time costs</div>
            </div>
          </div>

          <!-- Material Costs Section -->
          <div class="detail-section">
            <div class="detail-header">
              <h3>Material Costs</h3>
              <ion-button
                v-if="!showCreateMaterial && !showMaterialPicker"
                size="small"
                fill="outline"
                @click="showCreateMaterial = true"
              >
                Create New
              </ion-button>
            </div>
            <!-- Create New Material Form -->
            <div v-if="showCreateMaterial" class="create-material-form">
              <div class="form-row">
                <ion-input
                  v-model="newMaterial.refId"
                  placeholder="RefId (required)"
                  fill="outline"
                  class="form-input"
                ></ion-input>
              </div>
              <div class="form-row">
                <ion-input
                  v-model="newMaterial.name"
                  placeholder="Name (optional)"
                  fill="outline"
                  class="form-input"
                ></ion-input>
              </div>
              <div class="form-row">
                <ion-input
                  v-model.number="newMaterial.quantity"
                  type="number"
                  step="0.01"
                  placeholder="Quantity"
                  fill="outline"
                  class="form-input"
                ></ion-input>
                <span class="currency-hint">in your currency</span>
              </div>
              <div class="form-actions">
                <ion-button size="small" @click="createNewMaterial">Create & Add</ion-button>
                <ion-button size="small" fill="clear" color="medium" @click="cancelCreateMaterial">Cancel</ion-button>
              </div>
            </div>
            <!-- Material Picker View -->
            <div v-if="showMaterialPicker" class="material-picker">
              <div class="picker-header">
                <ion-searchbar
                  v-model="materialSearchQuery"
                  placeholder="Search materials..."
                  class="picker-search"
                ></ion-searchbar>
                <ion-button fill="clear" size="small" @click="closeMaterialPicker">
                  <ion-icon :icon="closeOutline"></ion-icon>
                </ion-button>
              </div>
              <div class="picker-list">
                <div
                  v-for="item in filteredCostsMaterial"
                  :key="item.id"
                  class="picker-item"
                  @click="selectMaterialCost(item)"
                >
                  <span class="picker-item-name">{{ item.refId || item.name }}</span>
                  <span class="picker-item-value">{{ item.quantityConverted?.formatted || item.quantity }}</span>
                </div>
                <div v-if="filteredCostsMaterial.length === 0" class="picker-empty">
                  No materials found
                </div>
              </div>
            </div>
            <!-- Material List -->
            <div v-else class="detail-list">
              <div v-for="item in displayMaterials" :key="item.id" class="detail-list-item" :class="{ 'pending-item': item.isPending, 'removed-item': item.isRemoved }">
                <span class="item-ref">
                  {{ item.refId || item.name }}
                  <span v-if="item.isPending" class="pending-badge">new</span>
                  <span v-if="item.isRemoved" class="removed-badge">removing</span>
                </span>
                <span class="item-actions">
                  <span class="item-value">{{ item.quantityConverted?.formatted || item.quantity || '—' }}</span>
                  <ion-icon
                    v-if="!item.isRemoved"
                    :icon="removeCircleOutline"
                    class="remove-icon"
                    @click="removeMaterialCost(item)"
                  ></ion-icon>
                </span>
              </div>
              <div v-if="displayMaterials.length === 0" class="empty-list">No material costs</div>
              <div class="add-item-trigger" @click="openMaterialPicker">
                <ion-icon :icon="addCircleOutline" class="add-icon"></ion-icon>
              </div>
            </div>
            <div class="price-factors">
              <div class="factor-row">
                <span class="factor-label">Sales Tax</span>
                <span class="factor-value">× {{ formulaParams.salesTaxMultiplier }}</span>
              </div>
              <div class="factor-row">
                <span class="factor-label">Markup</span>
                <span class="factor-value">× {{ selectedOffer.derivedVars?.markupMultiplier?.toFixed(2) || '—' }}</span>
              </div>
              <div class="factor-row factor-total">
                <span class="factor-label">Material Fee</span>
                <span class="factor-value">{{ selectedOffer.derivedVarsConverted?.materialFee?.formatted || '—' }}</span>
              </div>
            </div>
          </div>

          <!-- Final Price Section -->
          <div class="detail-section">
            <div class="detail-header">
              <h3>Final Price</h3>
            </div>
            <div class="detail-value price-value">{{ selectedOffer.finalPriceConverted?.formatted || '—' }}</div>
          </div>
        </div>
      </ion-content>
    </ion-modal>
  </BaseLayout>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from "vue";
import {
  IonGrid,
  IonRow,
  IonCol,
  IonSpinner,
  IonModal,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonButtons,
  IonButton,
  IonContent,
  IonIcon,
  IonInput,
  IonSearchbar,
} from "@ionic/vue";
import { pencilOutline, checkmarkOutline, addCircleOutline, closeOutline, removeCircleOutline } from "ionicons/icons";
import BaseLayout from "@/components/BaseLayout.vue";
import { loadPriceList, type EnrichedOffer } from "@/framework/priceList";
import { loadBooks, commitChanges, downloadChanges } from "@/framework/changes";
import { loadCurrencies, convert, convertToPrefs, type CurrenciesData } from "@/framework/currencies";
import { getPricingVars } from "@/framework/org";
import tnfrLegacyWithHours from "@/lib/tnfrLegacyWithHours";

interface DisplayOffer extends EnrichedOffer {
  totalHours: number;
  tierMultiplier: number | null;
  isBandAid: boolean;
  derivedFinalPrice: string | null; // For non-band-aid tiers: band-aid price × multiplier
}

const loading = ref(true);
const offers = ref<DisplayOffer[]>([]);
const offersTotal = ref(0);
const offersLimit = ref(25);
const currenciesData = ref<CurrenciesData | null>(null);
const selectedCurrency = ref("usd");
const currencySymbol = ref("$");
const formulaParams = ref({
  hourlyFee: 0,
  hourlyFeeConverted: "",
  salesTax: "",
  salesTaxMultiplier: 1,
  serviceCallFee: 0,
  serviceCallFeeConverted: "",
});
const extraHours = ref(0);
const extraHoursDiscount = ref(0.4);
const useTierMultiplier = ref(false);
const orgVariables = ref<{ hourlyFee: number; salesTax: number; serviceCallFee: number } | null>(null);

// Test mode state
const testMode = ref(false);
const testOffers = ref<DisplayOffer[]>([]);
const csvInput = ref<HTMLInputElement | null>(null);
const csvFileName = ref<string | null>(null);
const csvFileContent = ref<string | null>(null);

// Display either normal offers or test offers
const displayOffers = computed(() => {
  if (testMode.value) {
    return testOffers.value || [];
  }
  return offers.value || [];
});

// Modal state
const isModalOpen = ref(false);
const selectedOffer = ref<DisplayOffer | null>(null);

// Edit state
const isEditing = reactive({
  name: false,
  costsTime: false,
  costsMaterial: false,
});

const editValues = reactive({
  name: "",
  costsTime: [] as { refId: string; hours: number }[],
  costsMaterial: [] as { refId: string; quantity: string }[],
});

// Add new time cost state
const showAddTimeCost = ref(false);
const newTimeCost = reactive({
  refId: "",
  hours: 0,
});

// Material cost picker state
const showMaterialPicker = ref(false);
const allCostsMaterial = ref<any[]>([]);
const materialSearchQuery = ref("");

// Changeset state
const pendingChanges = ref<any[]>([]);
const showChangesetPreview = ref(false);
const availableBooks = ref<any[]>([]);
const selectedBookId = ref("");
const commitStatus = ref<{ success: boolean; message: string } | null>(null);
const isCommitting = ref(false);

// Create new material state
const showCreateMaterial = ref(false);
const newMaterial = reactive({
  refId: "",
  name: "",
  quantity: 0,
});

// Computed: materials including pending additions and removals
const displayMaterials = computed(() => {
  if (!selectedOffer.value) return [];

  const originalMaterials = selectedOffer.value.costsMaterial;
  const originalRefIds = originalMaterials.map(m => m.refId);

  // Find pending offer change
  const pendingChange = pendingChanges.value.find(
    c => c.collection === "offers" && c.data.refId === selectedOffer.value?.refId
  );

  // If no pending change, return originals as-is
  if (!pendingChange) {
    return originalMaterials.map(m => ({ ...m }));
  }

  const pendingRefs = pendingChange.data.refs?.find((r: any) => r.targetField === "costsMaterial");
  if (!pendingRefs) {
    return originalMaterials.map(m => ({ ...m }));
  }

  const finalRefIds: string[] = pendingRefs.refIds;
  const materials: any[] = [];

  // Add original materials, marking removed ones
  for (const mat of originalMaterials) {
    const isRemoved = !mat.refId || !finalRefIds.includes(mat.refId);
    materials.push({ ...mat, isRemoved });
  }

  // Add new materials (in finalRefIds but not in original)
  const newRefIds = finalRefIds.filter(id => !originalRefIds.includes(id));
  for (const refId of newRefIds) {
    const matItem = allCostsMaterial.value.find(m => m.refId === refId);
    if (matItem) {
      materials.push({ ...matItem, isPending: true });
    }
  }

  return materials;
});

function openModal(offer: DisplayOffer) {
  selectedOffer.value = offer;
  // Initialize edit values
  editValues.name = (offer as any).name || (offer as any).refId || "";
  editValues.costsTime = offer.costsTime.map(item => ({
    refId: item.refId || item.name || "",
    hours: item.hours || 0,
  }));
  editValues.costsMaterial = offer.costsMaterial.map(item => ({
    refId: item.refId || item.name || "",
    quantity: item.quantity || "0",
  }));
  // Reset edit state
  isEditing.name = false;
  isEditing.costsTime = false;
  isEditing.costsMaterial = false;
  // Reset add time cost form
  showAddTimeCost.value = false;
  newTimeCost.refId = "";
  newTimeCost.hours = 0;
  // Reset material picker
  showMaterialPicker.value = false;
  materialSearchQuery.value = "";
  // Reset changeset
  pendingChanges.value = [];
  showChangesetPreview.value = false;
  // Reset create material form
  showCreateMaterial.value = false;
  newMaterial.refId = "";
  newMaterial.name = "";
  newMaterial.quantity = 0;
  // Reset commit state
  selectedBookId.value = "";
  commitStatus.value = null;
  isCommitting.value = false;
  // Load books if not already loaded
  if (availableBooks.value.length === 0) {
    loadBooks().then(books => {
      availableBooks.value = books;
    });
  }
  isModalOpen.value = true;
}

function closeModal() {
  isModalOpen.value = false;
  selectedOffer.value = null;
}

function toggleEdit(field: "name" | "costsTime" | "costsMaterial") {
  isEditing[field] = !isEditing[field];
  // Reset add form when toggling off
  if (!isEditing[field] && field === "costsTime") {
    showAddTimeCost.value = false;
    newTimeCost.refId = "";
    newTimeCost.hours = 0;
  }
  if (!isEditing[field] && field === "costsMaterial") {
    showMaterialPicker.value = false;
    materialSearchQuery.value = "";
  }
}

function addTimeCost() {
  if (newTimeCost.refId.trim()) {
    editValues.costsTime.push({
      refId: newTimeCost.refId.trim(),
      hours: newTimeCost.hours || 0,
    });
    // Reset form
    newTimeCost.refId = "";
    newTimeCost.hours = 0;
    showAddTimeCost.value = false;
  }
}

function cancelAddTimeCost() {
  newTimeCost.refId = "";
  newTimeCost.hours = 0;
  showAddTimeCost.value = false;
}

// Material cost picker functions
async function openMaterialPicker() {
  const { getDb } = await import("@/dataAccess/getDb");
  const db = await getDb();
  const rawMaterials = (await db.selectAll("costsMaterial")) || [];

  // Convert quantities to user's preferred currency
  allCostsMaterial.value = await Promise.all(
    rawMaterials.map(async (item: any) => ({
      ...item,
      quantityConverted: item.quantity ? await convertToPrefs(Number(item.quantity)) : null,
    }))
  );

  materialSearchQuery.value = "";
  showMaterialPicker.value = true;
}

const filteredCostsMaterial = computed(() => {
  if (!materialSearchQuery.value.trim()) {
    return allCostsMaterial.value;
  }
  const query = materialSearchQuery.value.toLowerCase();
  return allCostsMaterial.value.filter(item =>
    (item.refId?.toLowerCase().includes(query)) ||
    (item.name?.toLowerCase().includes(query))
  );
});

function selectMaterialCost(item: any) {
  if (!selectedOffer.value) return;

  console.log("[selectMaterialCost] selectedOffer:", selectedOffer.value);
  console.log("[selectMaterialCost] offer id:", selectedOffer.value.id, "refId:", selectedOffer.value.refId);

  // Get all current refIds including pending additions
  const allRefIds = displayMaterials.value.map(c => c.refId || c.name || "");

  // Check if already in offer or pending
  if (allRefIds.includes(item.refId)) {
    closeMaterialPicker();
    return;
  }

  // Add the new refId to the list
  const newRefIds = [...allRefIds, item.refId];

  // Create an update changeset item
  const change = {
    collection: "offers",
    operation: "update",
    data: {
      refId: selectedOffer.value.refId,
      refs: [
        {
          collection: "costsMaterial",
          refIds: newRefIds,
          targetField: "costsMaterial",
        },
      ],
    },
  };

  // Replace any existing offer update or add new one
  const existingIdx = pendingChanges.value.findIndex(
    c => c.collection === "offers" && c.data.refId === selectedOffer.value?.refId
  );
  if (existingIdx >= 0) {
    pendingChanges.value[existingIdx] = change;
  } else {
    pendingChanges.value.push(change);
  }

  closeMaterialPicker();
}

function closeMaterialPicker() {
  showMaterialPicker.value = false;
  materialSearchQuery.value = "";
}

async function createNewMaterial() {
  if (!newMaterial.refId.trim() || !selectedOffer.value) return;

  // Convert quantity from user's preferred currency to base currency
  const { convertFromPrefs } = await import("@/framework/currencies");
  const baseQuantity = await convertFromPrefs(newMaterial.quantity);

  // Create the costsMaterial record changeset
  const createChange = {
    collection: "costsMaterial",
    operation: "create",
    data: {
      refId: newMaterial.refId.trim(),
      name: newMaterial.name.trim() || newMaterial.refId.trim(),
      quantity: baseQuantity ?? newMaterial.quantity,
    },
  };

  pendingChanges.value.push(createChange);

  // Also add it to allCostsMaterial so it shows up in displayMaterials
  const convertedQuantity = await convertToPrefs(baseQuantity ?? newMaterial.quantity);
  allCostsMaterial.value.push({
    id: `new-${Date.now()}`,
    refId: newMaterial.refId.trim(),
    name: newMaterial.name.trim() || newMaterial.refId.trim(),
    quantity: baseQuantity ?? newMaterial.quantity,
    quantityConverted: convertedQuantity,
  });

  // Now add this new material to the offer
  const allRefIds = displayMaterials.value
    .filter(m => !m.isRemoved)
    .map(c => c.refId || c.name || "");
  const newRefIds = [...allRefIds, newMaterial.refId.trim()];

  // Create/update the offer changeset
  const offerChange = {
    collection: "offers",
    operation: "update",
    data: {
      refId: selectedOffer.value.refId,
      refs: [
        {
          collection: "costsMaterial",
          refIds: newRefIds,
          targetField: "costsMaterial",
        },
      ],
    },
  };

  const existingIdx = pendingChanges.value.findIndex(
    c => c.collection === "offers" && c.data.refId === selectedOffer.value?.refId
  );
  if (existingIdx >= 0) {
    pendingChanges.value[existingIdx] = offerChange;
  } else {
    pendingChanges.value.push(offerChange);
  }

  // Reset form
  newMaterial.refId = "";
  newMaterial.name = "";
  newMaterial.quantity = 0;
  showCreateMaterial.value = false;
}

function cancelCreateMaterial() {
  newMaterial.refId = "";
  newMaterial.name = "";
  newMaterial.quantity = 0;
  showCreateMaterial.value = false;
}

async function commitPendingChanges() {
  if (pendingChanges.value.length === 0 || !selectedBookId.value) return;

  isCommitting.value = true;
  commitStatus.value = null;

  try {
    const result = await commitChanges(pendingChanges.value, {
      bookId: selectedBookId.value,
    });

    if (result.success) {
      commitStatus.value = {
        success: true,
        message: `Committed! Record ID: ${result.recordId}`,
      };
      // Clear pending changes after successful commit
      pendingChanges.value = [];
      // Reload the price list to reflect changes
      await reloadPriceList();
      // Close the modal after a short delay to show success message
      setTimeout(() => {
        closeModal();
      }, 1000);
    } else {
      commitStatus.value = {
        success: false,
        message: result.error || "Commit failed",
      };
    }
  } catch (error) {
    commitStatus.value = {
      success: false,
      message: error instanceof Error ? error.message : String(error),
    };
  } finally {
    isCommitting.value = false;
  }
}

function removeMaterialCost(item: any) {
  if (!selectedOffer.value) return;

  const refIdToRemove = item.refId;

  // Get current final refIds (either from pending change or from original)
  let currentFinalRefIds: string[];

  const existingChange = pendingChanges.value.find(
    c => c.collection === "offers" && c.data.refId === selectedOffer.value?.refId
  );

  if (existingChange) {
    const pendingRefs = existingChange.data.refs?.find((r: any) => r.targetField === "costsMaterial");
    currentFinalRefIds = pendingRefs ? [...pendingRefs.refIds] : [];
  } else {
    currentFinalRefIds = selectedOffer.value.costsMaterial.map(c => c.refId).filter((id): id is string => !!id);
  }

  // Remove the item
  const newRefIds = currentFinalRefIds.filter(id => id !== refIdToRemove);

  // Create/update the changeset
  const change = {
    collection: "offers",
    operation: "update",
    data: {
      refId: selectedOffer.value.refId,
      refs: [
        {
          collection: "costsMaterial",
          refIds: newRefIds,
          targetField: "costsMaterial",
        },
      ],
    },
  };

  const existingIdx = pendingChanges.value.findIndex(
    c => c.collection === "offers" && c.data.refId === selectedOffer.value?.refId
  );
  if (existingIdx >= 0) {
    pendingChanges.value[existingIdx] = change;
  } else {
    pendingChanges.value.push(change);
  }
}

// Format a single currency value (for formula params)
function formatValue(value: number): string {
  if (!currenciesData.value) return "—";
  const converted = convert(currenciesData.value.rates, "silver", selectedCurrency.value, value);
  return `${currencySymbol.value}${Math.ceil(converted).toLocaleString()}`;
}

// Create formula factory with additionalTime, discount, and org vars set
function createFormulaFactory() {
  return () => {
    const formula = tnfrLegacyWithHours();
    formula.vars.additionalTime.value = extraHours.value;
    formula.vars.additionalHourDiscount.value = extraHoursDiscount.value;
    // Apply org variables if available
    if (orgVariables.value) {
      formula.vars.hourlyFee.value = orgVariables.value.hourlyFee;
      formula.vars.salesTax.value = orgVariables.value.salesTax;
      formula.vars.serviceCallFee.value = orgVariables.value.serviceCallFee;
    }
    return formula;
  };
}

async function reloadPriceList() {
  if (!currenciesData.value) {
    return;
  }

  try {
    const priceListResult = await loadPriceList({ formulaFactory: createFormulaFactory(), limit: offersLimit.value });
    offersTotal.value = priceListResult.total;

  // Build lookup of band-aid tier prices and final prices by menuName
  const bandAidData = new Map<string, { tierPrice: number; finalPrice: number }>();

  for (const offer of priceListResult.offers) {
    if (offer.menuTier === "bandaid" && offer.menuName && offer.derivedVars?.tierPrice && offer.finalPrice) {
      bandAidData.set(offer.menuName, {
        tierPrice: offer.derivedVars.tierPrice,
        finalPrice: offer.finalPrice,
      });
    }
  }

  // Enrich offers with computed totals and tier multiplier
  offers.value = priceListResult.offers.map(offer => {
    let tierMultiplier: number | null = null;
    let derivedFinalPrice: string | null = null;
    const isBandAid = offer.menuTier === "bandaid";

    if (offer.menuName && offer.derivedVars?.tierPrice) {
      const bandAid = bandAidData.get(offer.menuName);
      if (bandAid && bandAid.tierPrice > 0) {
        tierMultiplier = offer.derivedVars.tierPrice / bandAid.tierPrice;

        // For non-band-aid tiers, calculate derived final price
        if (!isBandAid && useTierMultiplier.value && extraHours.value > 0) {
          const derivedPrice = bandAid.finalPrice * tierMultiplier;
          const convertedPrice = convert(
            currenciesData.value!.rates,
            "silver",
            selectedCurrency.value,
            derivedPrice
          );
          derivedFinalPrice = `${currencySymbol.value}${Math.ceil(convertedPrice).toLocaleString()}`;
        }
      }
    }

    return {
      ...offer,
      totalHours: offer.costsTime.reduce((sum, item) => sum + (item.hours || 0), 0),
      tierMultiplier,
      isBandAid,
      derivedFinalPrice,
    };
  });
  } catch (e) {
    console.error("[PriceList] Error loading price list:", e);
  }
}

async function showAllOffers() {
  offersLimit.value = 10000;
  await reloadPriceList();
}

// CSV Test Mode functions
function triggerCsvUpload() {
  csvInput.value?.click();
}

async function handleCsvUpload(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (!file) return;

  try {
    csvFileName.value = file.name;
    csvFileContent.value = await file.text();
  } catch (e) {
    console.error("[PriceList] Error reading CSV:", e);
    alert("Error reading CSV file");
  }

  // Reset the input so the same file can be uploaded again
  input.value = "";
}

function clearCsvFile() {
  csvFileName.value = null;
  csvFileContent.value = null;
  testMode.value = false;
  testOffers.value = [];
}

async function parseCsvFile() {
  console.log("[PriceList] parseCsvFile called, content length:", csvFileContent.value?.length);
  if (!csvFileContent.value) {
    console.log("[PriceList] No CSV content");
    return;
  }

  try {
    console.log("[PriceList] CSV content preview:", csvFileContent.value.substring(0, 200));
    const rows = parseCsv(csvFileContent.value);
    console.log("[PriceList] Parsed rows:", rows);

    if (rows.length === 0) {
      alert("No valid data found in CSV. Make sure it has 'task_code' and 'price' columns.");
      return;
    }

    // Convert CSV rows to test offers (just display the task_code and price)
    const testData: DisplayOffer[] = [];

    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      const price = parseFloat(row.price) || 0;

      testData.push({
        id: `test-${i}`,
        refId: row.taskCode,
        name: row.taskCode,
        costsTime: [],
        costsMaterial: [],
        menuName: null,
        menuTier: null,
        finalPrice: price,
        finalPriceConverted: {
          value: price,
          formatted: `$${price.toFixed(2)}`,
          symbol: "$",
          targetCurrency: "usd",
        },
        formulaSteps: null,
        derivedVars: null,
        derivedVarsConverted: null,
        totalHours: 0,
        tierMultiplier: null,
        isBandAid: false,
        derivedFinalPrice: null,
      });
    }

    console.log("[PriceList] Created test offers:", testData.length);
    testOffers.value = testData;
    testMode.value = true;
    console.log("[PriceList] testMode set to true, testOffers:", testOffers.value.length);
  } catch (e) {
    console.error("[PriceList] Error parsing CSV:", e);
    alert("Error parsing CSV file");
  }
}

function parseCsv(text: string): Array<{ taskCode: string; price: string }> {
  const lines = text.trim().split("\n");
  console.log("[parseCsv] Lines:", lines.length, "First line:", lines[0]);
  if (lines.length < 2) return []; // Need header + at least one row

  const header = lines[0].split(",").map(h => h.trim().toLowerCase());
  console.log("[parseCsv] Header:", header);
  const taskCodeIdx = header.findIndex(h => h === "task_code" || h === "taskcode" || h === "task code");
  const priceIdx = header.findIndex(h => h === "price");
  console.log("[parseCsv] Column indices - task_code:", taskCodeIdx, "price:", priceIdx);

  if (taskCodeIdx < 0 || priceIdx < 0) {
    console.error("[parseCsv] Required columns not found. Need 'task_code' and 'price'");
    return [];
  }

  const rows: Array<{ taskCode: string; price: string }> = [];

  for (let i = 1; i < lines.length; i++) {
    const cols = lines[i].split(",").map(c => c.trim());
    if (cols.length === 0 || (cols.length === 1 && cols[0] === "")) continue;

    const taskCode = cols[taskCodeIdx] || "";
    const price = cols[priceIdx] || "";

    if (taskCode && price) {
      rows.push({ taskCode, price });
      console.log("[parseCsv] Row", i, ":", taskCode, "=", price);
    }
  }

  return rows;
}

onMounted(async () => {
  try {
    // Load currencies first
    const currencies = await loadCurrencies();
    currenciesData.value = currencies;

    // Get currency symbol
    const currency = currencies.currencies.find(c => c.refId === selectedCurrency.value);
    currencySymbol.value = currency?.symbol || "$";

    // Get pricing variables from org (with defaults)
    const pricingVars = await getPricingVars();
    const { hourlyFee, salesTax, serviceCallFee } = pricingVars;

    // Store org vars for use in formula factory
    orgVariables.value = { hourlyFee, salesTax, serviceCallFee };

    formulaParams.value = {
      hourlyFee,
      hourlyFeeConverted: formatValue(hourlyFee),
      salesTax: `${((salesTax - 1) * 100).toFixed(0)}%`,
      salesTaxMultiplier: salesTax,
      serviceCallFee,
      serviceCallFeeConverted: formatValue(serviceCallFee),
    };

    // Load price list
    await reloadPriceList();
  } finally {
    loading.value = false;
  }
});
</script>

<style scoped>
.price-list-container {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.page-title {
  margin: 0 0 24px 0;
  color: var(--ion-text-color);
  font-size: 32px;
  font-weight: 700;
  text-align: center;
}

.content-section {
  padding: 0 20px 20px 20px;
}

.loading-state,
.empty-state {
  text-align: center;
  padding: 40px;
  color: var(--ion-color-medium);
}

.loading-state ion-spinner {
  margin-bottom: 16px;
}

.formula-info {
  font-size: 13px;
  color: var(--ion-color-medium);
  margin-bottom: 12px;
}

.formula-info a {
  color: #0000EE;
  text-decoration: underline;
}

.formula-info a:hover {
  text-decoration: underline;
}

.results-info {
  font-size: 13px;
  color: var(--ion-color-medium);
  margin-bottom: 12px;
  display: flex;
  align-items: center;
  gap: 12px;
}

.show-all-btn {
  padding: 4px 12px;
  font-size: 12px;
  background: var(--ion-color-primary);
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.show-all-btn:hover {
  background: var(--ion-color-primary-shade);
}

.formula-params {
  display: flex;
  gap: 24px;
  margin-bottom: 16px;
  padding: 12px 16px;
  background: var(--ion-color-light);
  border-radius: 8px;
}

.param-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

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

.param-value {
  font-size: 16px;
  font-weight: 500;
  color: var(--ion-color-dark);
}

.param-input {
  width: 80px;
  padding: 4px 8px;
  font-size: 16px;
  font-weight: 500;
  border: 1px solid var(--ion-color-medium);
  border-radius: 4px;
  text-align: center;
}

.toggle-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  color: var(--ion-color-dark);
  cursor: pointer;
  margin-left: auto;
}

.toggle-label input[type="checkbox"] {
  width: 16px;
  height: 16px;
  cursor: pointer;
}

.recalculate-btn {
  padding: 8px 16px;
  font-size: 14px;
  font-weight: 500;
  background: var(--ion-color-primary);
  color: var(--ion-color-primary-contrast);
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 16px;
}

.recalculate-btn:hover {
  background: var(--ion-color-primary-shade);
}

.test-btn {
  padding: 8px 16px;
  font-size: 14px;
  font-weight: 500;
  background: var(--ion-color-secondary);
  color: var(--ion-color-secondary-contrast);
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 8px;
}

.test-btn:hover {
  background: var(--ion-color-secondary-shade);
}

.csv-file-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--ion-color-light);
  border: 1px solid var(--ion-color-medium);
  padding: 8px 16px;
  border-radius: 4px;
  margin-bottom: 16px;
}

.csv-file-name {
  font-weight: 500;
  font-family: monospace;
  color: var(--ion-color-dark);
}

.csv-status {
  color: var(--ion-color-success);
  font-weight: 500;
  margin-left: auto;
}

.parse-btn {
  padding: 4px 12px;
  font-size: 13px;
  font-weight: 500;
  background: var(--ion-color-success);
  color: var(--ion-color-success-contrast);
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.parse-btn:hover {
  background: var(--ion-color-success-shade);
}

.clear-btn {
  padding: 4px 12px;
  font-size: 13px;
  font-weight: 500;
  background: var(--ion-color-medium);
  color: var(--ion-color-medium-contrast);
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.clear-btn:hover {
  background: var(--ion-color-medium-shade);
}

.data-table {
  background: var(--ion-color-light);
  border-radius: 8px;
  overflow: hidden;
}

.table-header {
  background: var(--ion-color-primary);
  color: var(--ion-color-primary-contrast);
  font-weight: 600;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.table-header ion-col {
  padding: 12px 16px;
}

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

.table-row:last-child {
  border-bottom: none;
}

.table-row ion-col {
  padding: 12px 16px;
  color: var(--ion-color-dark);
  font-size: 14px;
}

.table-row.clickable {
  cursor: pointer;
}

.table-row:hover {
  background: var(--ion-color-light-shade);
}

.text-right {
  text-align: right;
}

.cell-primary {
  font-weight: 500;
  color: var(--ion-color-dark);
}

.cell-secondary {
  font-size: 12px;
  color: var(--ion-color-medium);
  margin-top: 4px;
  line-height: 1.4;
}

.ref-tag {
  white-space: nowrap;
}

/* Modal Styles */
.modal-content {
  padding: 8px;
}

.detail-section {
  background: var(--ion-color-light);
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
}

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

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

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

.edit-icon:hover {
  color: var(--ion-color-primary-shade);
}

.detail-value {
  font-size: 18px;
  font-weight: 500;
  color: var(--ion-color-dark);
}

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

.detail-summary {
  font-size: 16px;
  font-weight: 600;
  color: var(--ion-color-dark);
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--ion-color-light-shade);
}

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

.detail-list-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 12px;
  background: var(--ion-color-light-shade);
  border-radius: 6px;
}

.item-ref {
  font-size: 14px;
  color: var(--ion-color-dark);
}

.item-value {
  font-size: 14px;
  font-weight: 500;
  color: var(--ion-color-primary);
}

.empty-list {
  font-size: 14px;
  color: var(--ion-color-medium);
  font-style: italic;
  padding: 8px;
}

.price-factors {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px dashed var(--ion-color-medium-shade);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.factor-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 13px;
}

.factor-label {
  color: var(--ion-color-medium);
}

.factor-value {
  font-weight: 600;
  color: var(--ion-color-dark);
}

.factor-total {
  margin-top: 4px;
  padding-top: 8px;
  border-top: 1px solid var(--ion-color-light-shade);
}

.factor-total .factor-value {
  color: var(--ion-color-primary);
}

.edit-field {
  margin-top: 8px;
}

.edit-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.edit-list-item {
  display: flex;
  gap: 8px;
}

.edit-input-small {
  flex: 1;
}

.add-item-trigger {
  display: flex;
  justify-content: center;
  padding: 8px;
  cursor: pointer;
}

.add-icon {
  font-size: 28px;
  color: var(--ion-color-primary);
  transition: color 0.2s ease;
}

.add-icon:hover {
  color: var(--ion-color-primary-shade);
}

.add-item-form {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 8px;
  background: var(--ion-color-light-shade);
  border-radius: 6px;
  margin-top: 8px;
}

/* Material Picker */
.material-picker {
  background: var(--ion-color-light);
  border-radius: 8px;
  border: 1px solid var(--ion-color-light-shade);
}

.picker-header {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  border-bottom: 1px solid var(--ion-color-light-shade);
}

.picker-search {
  flex: 1;
  --background: var(--ion-color-light);
  --box-shadow: none;
  padding: 0;
}

.picker-list {
  max-height: 250px;
  overflow-y: auto;
}

.picker-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 12px;
  cursor: pointer;
  border-bottom: 1px solid var(--ion-color-light-shade);
}

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

.picker-item:hover {
  background: var(--ion-color-light-shade);
}

.picker-item-name {
  font-size: 14px;
  font-weight: 500;
}

.picker-item-value {
  font-size: 13px;
  color: var(--ion-color-medium);
  font-family: var(--ion-font-family-mono, monospace);
}

.picker-empty {
  padding: 16px;
  text-align: center;
  color: var(--ion-color-medium);
  font-style: italic;
}

/* Pending items */
.pending-item {
  background: var(--ion-color-success-tint);
}

.pending-badge {
  font-size: 10px;
  background: var(--ion-color-success);
  color: white;
  padding: 2px 6px;
  border-radius: 4px;
  margin-left: 6px;
  text-transform: uppercase;
  font-weight: 600;
}

/* Removed items */
.removed-item {
  background: var(--ion-color-danger-tint);
  opacity: 0.7;
}

.removed-item .item-ref,
.removed-item .item-value {
  text-decoration: line-through;
}

.removed-badge {
  font-size: 10px;
  background: var(--ion-color-danger);
  color: white;
  padding: 2px 6px;
  border-radius: 4px;
  margin-left: 6px;
  text-transform: uppercase;
  font-weight: 600;
}

.item-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

.remove-icon {
  font-size: 20px;
  color: var(--ion-color-danger);
  cursor: pointer;
  transition: color 0.2s ease;
}

.remove-icon:hover {
  color: var(--ion-color-danger-shade);
}

/* Changeset Preview */
.changeset-preview {
  padding: 8px;
}

.commit-controls {
  display: flex;
  gap: 12px;
  align-items: center;
  margin-bottom: 16px;
}

.book-select {
  flex: 1;
  padding: 10px 12px;
  font-size: 14px;
  border: 1px solid var(--ion-color-light-shade);
  border-radius: 6px;
  background: var(--ion-color-light);
  max-width: 300px;
}

.commit-status {
  padding: 10px 12px;
  border-radius: 6px;
  margin-bottom: 16px;
  font-size: 14px;
}

.commit-status.success {
  background: var(--ion-color-success-tint);
  color: var(--ion-color-success-shade);
}

.commit-status.error {
  background: var(--ion-color-danger-tint);
  color: var(--ion-color-danger-shade);
}

.changeset-preview h3 {
  margin: 0 0 12px 0;
  font-size: 16px;
  font-weight: 600;
}

.changeset-json {
  background: var(--ion-color-light);
  border: 1px solid var(--ion-color-light-shade);
  border-radius: 8px;
  padding: 12px;
  font-size: 12px;
  font-family: monospace;
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Create Material Form */
.create-material-form {
  background: var(--ion-color-light);
  border: 1px solid var(--ion-color-light-shade);
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 12px;
}

.create-material-form .form-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.create-material-form .form-input {
  flex: 1;
  --background: var(--ion-color-light-tint);
}

.create-material-form .currency-hint {
  font-size: 12px;
  color: var(--ion-color-medium);
  white-space: nowrap;
}

.create-material-form .form-actions {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}
</style>