Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <div class="framework-view">
    <div class="top-row">
      <div class="top-row-left">
        <button type="button" @click="handleDirectoryClick">Directory</button>
        <button type="button" @click="handleCalculatePriceClick">Calculate Price</button>
        <button type="button" @click="handlePriceListClick">Price List</button>
        <button type="button" @click="handleJobContentClick">Job Content</button>
        <button type="button" @click="handleChangesClick">Changes</button>
        <button type="button" @click="handleCurrenciesClick">Currencies</button>
        <button type="button" @click="handleLogsClick">Logs</button>
      </div>
      <div class="top-row-right">
        <button type="button" @click="handleClear">Clear</button>
        <select v-model="selectedCurrency" class="currency-select">
          <option v-if="!currenciesData" value="">Loading...</option>
          <option v-for="currency in currenciesData?.currencies || []" :key="currency.id" :value="currency.refId">
            {{ currency.symbol }} {{ currency.refId }}
          </option>
        </select>
        <button type="button" @click="handleRelogin">Re-login</button>
      </div>
    </div>
    <div class="second-row">
      <div class="second-row-right">
        <input
          v-model="editId"
          type="text"
          placeholder="Record ID"
          class="edit-input"
          @keydown.enter="handleEditNavigate"
        />
        <button type="button" @click="handleEditNavigate">Edit</button>
      </div>
    </div>
    <div v-if="showCalculatePrice" class="calculate-price-row">
      <select v-model="selectedFormula" class="formula-select" @change="handleFormulaChange">
        <option value="">-- Select a formula --</option>
        <option value="tnfrLegacyWithHours">TNFR Legacy with Hours</option>
      </select>
      <button v-if="selectedFormula" type="button" @click="handleShowDocs">Documentation</button>
      <button v-if="selectedFormula" type="button" @click="handleShowFormulaJson">Formula</button>
      <button v-if="selectedFormula" type="button" @click="handleShowCalculator">Calculate</button>
    </div>
    <div v-if="showCalculatePrice && selectedFormula" class="calculate-price-row">
      <select v-model="selectedOfferId" class="formula-select" @change="handleOfferSelect">
        <option value="">-- Select an offer --</option>
        <option v-for="offer in availableOffers" :key="offer.id" :value="offer.id">
          {{ offer.refId || offer.name || offer.id }}
        </option>
      </select>
    </div>
    <div v-if="formulaDocs" class="formula-docs">
      <pre>{{ formulaDocs }}</pre>
    </div>
    <div v-if="showFormulaJson && formulaJsonData" class="formula-json">
      <div class="json-controls">
        <button type="button" @click="expandAllJson">Expand All</button>
        <button type="button" @click="collapseAllJson">Collapse All</button>
      </div>
      <pre><code><json-node :key="jsonKey" :data="formulaJsonData" :indent="0" :start-collapsed="!jsonExpandAll" :expand-all="jsonExpandMode" :on-copy="onCopyToEdit" /></code></pre>
    </div>
    <div v-if="showCalculator" class="calculator-view">
      <div class="collapsible-section">
        <div class="section-header" @click="inputSectionOpen = !inputSectionOpen">
          <span class="collapse-icon">{{ inputSectionOpen ? '−' : '+' }}</span>
          <span>Input</span>
        </div>
        <div v-if="inputSectionOpen" class="section-content">
          <div v-for="(meta, key) in calculatorInputs" :key="key" class="input-row">
            <label :for="'input-' + key">
              {{ key }}
              <span class="unit-label">({{ meta.unit }})</span>
            </label>
            <template v-if="meta.unit === 'currency'">
              <span class="currency-symbol">{{ currentCurrencySymbol }}</span>
              <input
                :id="'input-' + key"
                :value="calculatorDisplayInputs[key]"
                type="number"
                step="any"
                @input="updateDisplayInput(key, $event)"
              />
            </template>
            <input
              v-else-if="meta.unit !== 'scale'"
              :id="'input-' + key"
              v-model.number="calculatorInputs[key].value"
              type="number"
              step="any"
            />
            <span v-else class="array-value">[scale]</span>
          </div>
          <button type="button" class="run-button" @click="runCalculation">Run Calculation</button>
        </div>
      </div>
      <div class="collapsible-section">
        <div class="section-header" @click="outputSectionOpen = !outputSectionOpen">
          <span class="collapse-icon">{{ outputSectionOpen ? '−' : '+' }}</span>
          <span>Output</span>
        </div>
        <div v-if="outputSectionOpen" class="section-content">
          <div v-if="calculatorOutput !== null">
            <div class="json-controls">
              <button type="button" @click="expandAllJson">Expand All</button>
              <button type="button" @click="collapseAllJson">Collapse All</button>
            </div>
            <pre><code><json-node :key="jsonKey" :data="calculatorOutput" :indent="0" :start-collapsed="!jsonExpandAll" :expand-all="jsonExpandMode" :on-copy="onCopyToEdit" /></code></pre>
          </div>
          <div v-else class="no-output">Run calculation to see output</div>
        </div>
      </div>
    </div>
    <div v-if="showSearch" class="search-container">
      <input
        v-model="searchQuery"
        type="text"
        placeholder="Search problems..."
        @keydown.enter="handleSearch"
      />
      <button type="button" @click="handleSearch">Search</button>
    </div>
    <div v-if="directoryData" class="filter-section">
      <div class="filter-label">Categories:</div>
      <div class="category-buttons">
        <button
          v-for="category in directoryData.categories"
          :key="category.id"
          type="button"
          class="category-btn"
          :class="{ selected: selectedCategory === category.id }"
          @click="handleCategoryClick(category)"
        >
          {{ category.name }} ({{ getCategoryProblemCount(category.id) }})
        </button>
      </div>
    </div>
    <div v-if="directoryData && directoryData.tags.length" class="filter-section">
      <div class="filter-label">Tags:</div>
      <select v-model="selectedTagId" class="tag-select" @change="handleTagSelect">
        <option value="">-- Select a tag --</option>
        <option v-for="tag in directoryData.tags" :key="tag.id" :value="tag.id">
          {{ tag.name }}
        </option>
      </select>
      <div v-if="selectedTags.length" class="selected-tags">
        <span v-for="tagId in selectedTags" :key="tagId" class="tag-chip">
          {{ getTagName(tagId) }}
        </span>
      </div>
    </div>
    <div v-if="directoryData" class="results-info">
      <template v-if="searchQuery">
        {{ directoryData.searchResults.length }} search results
      </template>
      <template v-else>
        {{ getTotalCategoryProblems() }} problems
        <span v-if="selectedCategory"> in selected category</span>
      </template>
    </div>
    <div v-if="directoryData" class="json-block">
      <div class="json-controls">
        <button type="button" @click="expandAllJson">Expand All</button>
        <button type="button" @click="collapseAllJson">Collapse All</button>
      </div>
      <pre><code><json-node :key="jsonKey" :data="directoryData" :indent="0" :start-collapsed="!jsonExpandAll" :expand-all="jsonExpandMode" :on-copy="onCopyToEdit" /></code></pre>
    </div>
    <div v-if="priceListData" class="json-block">
      <div class="json-controls">
        <button type="button" @click="expandAllJson">Expand All</button>
        <button type="button" @click="collapseAllJson">Collapse All</button>
      </div>
      <pre><code><json-node :key="jsonKey" :data="priceListData" :indent="0" :start-collapsed="!jsonExpandAll" :expand-all="jsonExpandMode" :on-copy="onCopyToEdit" /></code></pre>
    </div>
    <div v-if="showJobContent" class="job-content-section">
      <div class="job-content-row">
        <select v-model="selectedProblemId" class="problem-select">
          <option value="">-- Select a problem --</option>
          <option v-for="problem in availableProblems" :key="problem.id" :value="problem.id">
            {{ problem.name || problem.refId }}
          </option>
        </select>
        <button type="button" :disabled="!selectedProblemId" @click="handleGetContent">Get Content</button>
      </div>
      <div v-if="jobContentData" class="json-block">
        <div class="json-controls">
          <button type="button" @click="expandAllJson">Expand All</button>
          <button type="button" @click="collapseAllJson">Collapse All</button>
        </div>
        <pre><code><json-node :key="jsonKey" :data="jobContentData" :indent="0" :start-collapsed="!jsonExpandAll" :expand-all="jsonExpandMode" :on-copy="onCopyToEdit" /></code></pre>
      </div>
    </div>
    <div v-if="changesData || changesTab !== 'history'" class="changes-section">
      <div class="changes-controls">
        <button type="button" :class="{ active: changesTab === 'history' }" @click="changesTab = 'history'">History</button>
        <button type="button" :class="{ active: changesTab === 'schemas' }" @click="changesTab = 'schemas'">Schemas</button>
        <button type="button" :class="{ active: changesTab === 'create' }" @click="changesTab = 'create'">Create</button>
        <button type="button" class="download-btn" :disabled="isDownloading" @click="handleDownloadChanges">
          {{ isDownloading ? 'Downloading...' : 'Download from API' }}
        </button>
        <span v-if="downloadStatus" :class="['download-status', downloadStatus.success ? 'success' : 'error']">
          {{ downloadStatus.message }}
        </span>
      </div>
      <!-- History Tab -->
      <template v-if="changesTab === 'history'">
        <div class="results-info">{{ changesData?.total || 0 }} changes</div>
        <div class="json-block">
          <div class="json-controls">
            <button type="button" @click="expandAllJson">Expand All</button>
            <button type="button" @click="collapseAllJson">Collapse All</button>
          </div>
          <pre><code><json-node :key="jsonKey" :data="changesData" :indent="0" :start-collapsed="!jsonExpandAll" :expand-all="jsonExpandMode" :on-copy="onCopyToEdit" /></code></pre>
        </div>
      </template>
      <!-- Schemas Tab -->
      <template v-else-if="changesTab === 'schemas'">
        <div class="results-info">{{ collectionSchemas.length }} collections</div>
        <div class="schemas-list">
          <div v-for="schema in collectionSchemas" :key="schema.name" class="schema-card">
            <div class="schema-header">
              <span class="schema-name">{{ schema.name }}</span>
              <span class="schema-desc">{{ schema.description }}</span>
            </div>
            <table class="schema-fields">
              <thead>
                <tr>
                  <th>Field</th>
                  <th>Type</th>
                  <th>Required</th>
                  <th>Description</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="field in schema.fields" :key="field.name" :class="{ required: field.required }">
                  <td class="field-name">{{ field.name }}</td>
                  <td class="field-type">
                    {{ field.type }}
                    <span v-if="field.refCollection" class="ref-target">→ {{ field.refCollection }}</span>
                  </td>
                  <td class="field-required">{{ field.required ? 'Yes' : '' }}</td>
                  <td class="field-desc">{{ field.description }}</td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      </template>
      <!-- Create Tab -->
      <template v-else-if="changesTab === 'create'">
        <div class="create-form">
          <div class="form-row">
            <label>Collection</label>
            <select v-model="createCollection" class="collection-select" @change="handleCollectionChange">
              <option value="">-- Select collection --</option>
              <option v-for="schema in collectionSchemas" :key="schema.name" :value="schema.name">
                {{ schema.name }}
              </option>
            </select>
          </div>
          <template v-if="selectedSchema">
            <div class="schema-info">{{ selectedSchema.description }}</div>
            <div v-for="field in selectedSchema.fields" :key="field.name" class="form-row">
              <label :class="{ required: field.required }">
                {{ field.name }}
                <span class="field-hint">{{ field.type }}<template v-if="field.refCollection"> → {{ field.refCollection }}</template></span>
              </label>
              <template v-if="field.type === 'ref'">
                <input
                  v-model="createRefInputs[field.name]"
                  type="text"
                  :placeholder="`refId(s) for ${field.refCollection}, comma-separated`"
                  class="form-input"
                />
              </template>
              <template v-else-if="field.type === 'number'">
                <div class="input-with-hint">
                  <input
                    v-model.number="createFormData[field.name]"
                    type="number"
                    step="any"
                    class="form-input"
                    :placeholder="createCollection === 'costsMaterial' && field.name === 'quantity' ? 'Enter in your currency' : ''"
                  />
                  <span v-if="createCollection === 'costsMaterial' && field.name === 'quantity' && currencyPrefsData" class="currency-hint">
                    {{ currencyPrefsData.symbol }} → XAG
                  </span>
                </div>
              </template>
              <template v-else>
                <input
                  v-model="createFormData[field.name]"
                  type="text"
                  :placeholder="field.description"
                  class="form-input"
                />
              </template>
            </div>
            <div class="form-actions">
              <button type="button" @click="generateChangeRecord">Generate Change Record</button>
            </div>
            <div v-if="createdChangeRecord" class="created-record">
              <div class="record-header">
                <span>Generated Change Record</span>
                <button type="button" @click="copyChangeRecord">Copy</button>
              </div>
              <pre><code>{{ JSON.stringify(createdChangeRecord, null, 2) }}</code></pre>
              <div class="commit-section">
                <select v-model="commitBookId" 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>
                <button type="button" class="commit-button" @click="commitChangeRecord" :disabled="!commitBookId">
                  Commit to Pocketbase
                </button>
              </div>
              <div v-if="commitStatus" :class="['commit-status', commitStatus.success ? 'success' : 'error']">
                {{ commitStatus.message }}
              </div>
            </div>
          </template>
        </div>
      </template>
    </div>
    <!-- Currencies Section -->
    <div v-if="showCurrencies && currenciesData" class="currencies-section">
      <div class="currencies-toolbar">
        <span class="toolbar-label">convert()</span>
        <input v-model.number="convertValue" type="number" step="any" placeholder="Value" class="convert-input" />
        <select v-model="convertFrom" class="convert-select">
          <option value="">From</option>
          <option v-for="c in currenciesData.currencies" :key="c.id" :value="c.refId">{{ c.refId }}</option>
        </select>
        <span class="toolbar-arrow">→</span>
        <select v-model="convertTo" class="convert-select">
          <option value="">To</option>
          <option v-for="c in currenciesData.currencies" :key="c.id" :value="c.refId">{{ c.refId }}</option>
        </select>
        <button type="button" @click="handleConvert" :disabled="!convertFrom || !convertTo">Convert</button>
        <span v-if="convertResult !== null" class="convert-result">= {{ convertResult }}</span>
      </div>
      <div class="results-info">{{ currenciesData.currencies.length }} currencies, {{ currenciesData.rates.length }} rates</div>
      <div class="currencies-grid">
        <div class="currencies-card">
          <h3>Currencies</h3>
          <table class="currencies-table">
            <thead>
              <tr>
                <th>RefId</th>
                <th>Name</th>
                <th>Symbol</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="c in currenciesData.currencies" :key="c.id">
                <td class="mono">{{ c.refId }}</td>
                <td>{{ c.name }}</td>
                <td class="mono">{{ c.symbol }}</td>
              </tr>
            </tbody>
          </table>
        </div>
        <div class="currencies-card">
          <h3>Exchange Rates</h3>
          <table class="currencies-table">
            <thead>
              <tr>
                <th>Base</th>
                <th>Quote</th>
                <th>Rate</th>
                <th v-if="currenciesData.rates[0]?.name">Name</th>
                <th v-if="currenciesData.rates[0]?.created">Date</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(r, idx) in currenciesData.rates" :key="idx">
                <td class="mono">{{ r.baseCurrency }}</td>
                <td class="mono">{{ r.quoteCurrency }}</td>
                <td class="mono">{{ r.rate }}</td>
                <td v-if="r.name">{{ r.name }}</td>
                <td v-if="r.created" class="mono">{{ r.created?.split('T')[0] }}</td>
              </tr>
            </tbody>
          </table>
        </div>
        <div class="currencies-card">
          <h3>Currency Prefs (userPrefs)</h3>
          <div v-if="currencyPrefsData" class="prefs-list">
            <div class="prefs-row">
              <span class="prefs-label">baseCurrency</span>
              <span class="prefs-value mono">{{ currencyPrefsData.baseCurrency }}</span>
            </div>
            <div class="prefs-row">
              <span class="prefs-label">targetCurrency</span>
              <span class="prefs-value mono">{{ currencyPrefsData.targetCurrency }}</span>
            </div>
            <div class="prefs-row">
              <span class="prefs-label">exchangeRate</span>
              <span class="prefs-value mono">{{ currencyPrefsData.exchangeRate }}</span>
            </div>
            <div class="prefs-row">
              <span class="prefs-label">symbol</span>
              <span class="prefs-value mono">{{ currencyPrefsData.symbol }}</span>
            </div>
          </div>
          <div v-else class="no-prefs">No currency prefs saved</div>
        </div>
      </div>
    </div>
    <!-- Logs Section -->
    <div v-if="showLogs" class="logs-section">
      <div class="logs-controls">
        <button type="button" :class="{ active: logsTab === 'list' }" @click="logsTab = 'list'">List</button>
        <button type="button" :class="{ active: logsTab === 'create' }" @click="logsTab = 'create'">Create</button>
        <button type="button" class="refresh-btn" @click="handleRefreshLogs">Refresh</button>
      </div>
      <!-- List Tab -->
      <template v-if="logsTab === 'list'">
        <div class="logs-filters">
          <input v-model="logsFilterType" type="text" placeholder="Filter by log_type" class="filter-input" />
          <input v-model="logsFilterCreatedBy" type="text" placeholder="Filter by created_by" class="filter-input" />
          <button type="button" @click="handleRefreshLogs">Apply</button>
        </div>
        <div class="results-info">{{ logsData.length }} logs</div>
        <table class="logs-table">
          <thead>
            <tr>
              <th>ID</th>
              <th>Created At</th>
              <th>Type</th>
              <th>Created By</th>
              <th>Data</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="log in logsData" :key="log.id">
              <td class="mono">{{ log.id }}</td>
              <td class="mono">{{ log.created_at?.replace('T', ' ').substring(0, 19) }}</td>
              <td class="mono">{{ log.log_type }}</td>
              <td>{{ log.created_by_name }} <span class="created-by-id">({{ log.created_by }})</span></td>
              <td class="log-data-cell">
                <details>
                  <summary>{{ Object.keys(log.log_data || {}).length }} fields</summary>
                  <pre>{{ JSON.stringify(log.log_data, null, 2) }}</pre>
                </details>
              </td>
            </tr>
          </tbody>
        </table>
      </template>
      <!-- Create Tab -->
      <template v-else-if="logsTab === 'create'">
        <div class="create-form">
          <div class="form-row">
            <label class="required">log_type</label>
            <input v-model="newLogType" type="text" placeholder="e.g., service_call_completed" class="form-input" />
          </div>
          <div class="form-row">
            <label class="required">created_by</label>
            <input v-model="newLogCreatedBy" type="text" placeholder="User ID or system ID" class="form-input" />
          </div>
          <div class="form-row">
            <label class="required">created_by_name</label>
            <input v-model="newLogCreatedByName" type="text" placeholder="Human-readable name" class="form-input" />
          </div>
          <div class="form-row">
            <label class="required">log_data (JSON)</label>
            <textarea v-model="newLogData" placeholder='{"key": "value"}' class="form-textarea"></textarea>
          </div>
          <div class="form-actions">
            <button type="button" @click="handleSaveLog">Save Log</button>
          </div>
          <div v-if="logSaveStatus" :class="['commit-status', logSaveStatus.success ? 'success' : 'error']">
            {{ logSaveStatus.message }}
          </div>
        </div>
      </template>
    </div>
  </div>
</template>

<script lang="ts">
import { ref, computed, defineComponent, onMounted } from "vue";
import { useRouter } from "vue-router";
import { loadDirectory, type DirectoryData, type DirectoryQuery } from "@/framework/directory";
import { loadPriceList, type PriceListData } from "@/framework/priceList";
import { loadCurrencies, convert, loadCurrencyPrefs, convertFromPrefs, type CurrenciesData, type CurrencyPrefs } from "@/framework/currencies";
import { loadJobContentByProblemId, type JobContentHierarchy } from "@/framework/jobContent";
import { loadChanges, loadBooks, downloadChanges, collectionSchemas, commitChanges, type ChangesData, type CollectionSchema, type ChangesetItem } from "@/framework/changes";
import { list as listLogs, save as saveLog, type LogEntryStored } from "@/framework/logs";
import { convertCurrency, convertToBase, formatCurrency, getCurrencySymbol } from "@/lib/currencyConvert";
import { getApi } from "@/dataAccess/getApi";
import tnfrLegacyWithHours, { docs as tnfrDocs } from "@/lib/tnfrLegacyWithHours";
import calculatePrice from "@/framework/calculatePrice";
import JsonNode from "@/components/JsonViewer.vue";

export default defineComponent({
  name: "Framework",
  components: { JsonNode },
  setup() {
    const router = useRouter();
    const directoryData = ref<DirectoryData | null>(null);
    const priceListData = ref<PriceListData | null>(null);
    const showSearch = ref(false);
    const searchQuery = ref("");
    const selectedCategory = ref<string | null>(null);
    const selectedTags = ref<string[]>([]);
    const selectedTagId = ref("");
    const showCalculatePrice = ref(false);
    const selectedFormula = ref("");
    const formulaDocs = ref<string | null>(null);
    const showCalculator = ref(false);
    const inputSectionOpen = ref(true);
    const outputSectionOpen = ref(true);
    const calculatorInputs = ref<Record<string, any>>({});
    const calculatorDisplayInputs = ref<Record<string, number>>({});
    const calculatorOutput = ref<any>(null);
    const currentFormula = ref<any>(null);
    const availableOffers = ref<any[]>([]);
    const selectedOfferId = ref("");
    const showFormulaJson = ref(false);
    const formulaJsonData = ref<any>(null);
    const jsonExpandAll = ref(true);  // true = expanded, false = collapsed (when mode is active)
    const jsonExpandMode = ref(false); // false initially, true after user clicks expand/collapse all
    const jsonKey = ref(0);
    const currenciesData = ref<CurrenciesData | null>(null);
    const selectedCurrency = ref("");
    const showJobContent = ref(false);
    const availableProblems = ref<any[]>([]);
    const selectedProblemId = ref("");
    const jobContentData = ref<JobContentHierarchy | null>(null);
    const editId = ref("");
    const changesData = ref<ChangesData | null>(null);
    const changesTab = ref<"history" | "schemas" | "create">("history");
    const showCurrencies = ref(false);
    const convertValue = ref<number>(1);
    const convertFrom = ref("");
    const convertTo = ref("");
    const convertResult = ref<number | null>(null);
    const createCollection = ref("");
    const createFormData = ref<Record<string, any>>({});
    const createRefInputs = ref<Record<string, string>>({});
    const createdChangeRecord = ref<any>(null);
    const currencyPrefsData = ref<CurrencyPrefs | null>(null);
    const commitBookId = ref("");
    const commitStatus = ref<{ success: boolean; message: string } | null>(null);
    const availableBooks = ref<any[]>([]);
    const downloadStatus = ref<{ success: boolean; message: string } | null>(null);
    const isDownloading = ref(false);
    const showLogs = ref(false);
    const logsTab = ref<"list" | "create">("list");
    const logsData = ref<LogEntryStored[]>([]);
    const logsFilterType = ref("");
    const logsFilterCreatedBy = ref("");
    const newLogType = ref("");
    const newLogCreatedBy = ref("");
    const newLogCreatedByName = ref("");
    const newLogData = ref("{}");
    const logSaveStatus = ref<{ success: boolean; message: string } | null>(null);

    function expandAllJson() {
      jsonExpandAll.value = true;
      jsonExpandMode.value = true;
      jsonKey.value++;
    }

    function collapseAllJson() {
      jsonExpandAll.value = false;
      jsonExpandMode.value = true;
      jsonKey.value++;
    }

    async function loadCurrencyData() {
      if (!currenciesData.value) {
        currenciesData.value = await loadCurrencies();
        // Default to usd if available, otherwise first currency
        if (!selectedCurrency.value && currenciesData.value.currencies.length > 0) {
          const usd = currenciesData.value.currencies.find(c => c.refId === "usd");
          selectedCurrency.value = usd?.refId || currenciesData.value.currencies[0].refId || "";
        }
      }
    }

    function formatCurrencyValue(value: number): string {
      if (!currenciesData.value || !selectedCurrency.value) return "";
      return formatCurrency(
        value,
        selectedCurrency.value,
        currenciesData.value.rates,
        currenciesData.value.currencies
      );
    }

    const currentCurrencySymbol = computed(() => {
      if (!currenciesData.value || !selectedCurrency.value) return "";
      return getCurrencySymbol(selectedCurrency.value, currenciesData.value.currencies);
    });

    function convertToDisplayCurrency(baseValue: number): number {
      if (!currenciesData.value || !selectedCurrency.value) return baseValue;
      return convertCurrency(baseValue, selectedCurrency.value, currenciesData.value.rates);
    }

    function convertFromDisplayCurrency(displayValue: number): number {
      if (!currenciesData.value || !selectedCurrency.value) return displayValue;
      return convertToBase(displayValue, selectedCurrency.value, currenciesData.value.rates);
    }

    function initializeDisplayInputs() {
      // Initialize display inputs for currency fields from calculatorInputs
      const displayInputs: Record<string, number> = {};
      for (const key of Object.keys(calculatorInputs.value)) {
        const meta = calculatorInputs.value[key];
        if (meta && meta.unit === 'currency' && typeof meta.value === 'number') {
          displayInputs[key] = convertToDisplayCurrency(meta.value);
        }
      }
      calculatorDisplayInputs.value = displayInputs;
    }

    function updateDisplayInput(key: string, event: Event) {
      const target = event.target as HTMLInputElement;
      const displayValue = parseFloat(target.value) || 0;
      calculatorDisplayInputs.value[key] = displayValue;

      // Convert back to base currency and update calculatorInputs
      const baseValue = convertFromDisplayCurrency(displayValue);
      if (calculatorInputs.value[key]) {
        calculatorInputs.value[key].value = baseValue;
      }
    }

    function clearAllContent() {
      // Clear all content below the top row
      showSearch.value = false;
      directoryData.value = null;
      priceListData.value = null;
      showCalculatePrice.value = false;
      selectedFormula.value = "";
      formulaDocs.value = null;
      showFormulaJson.value = false;
      formulaJsonData.value = null;
      showCalculator.value = false;
      calculatorInputs.value = {};
      calculatorDisplayInputs.value = {};
      calculatorOutput.value = null;
      currentFormula.value = null;
      availableOffers.value = [];
      selectedOfferId.value = "";
      searchQuery.value = "";
      selectedCategory.value = null;
      selectedTags.value = [];
      showJobContent.value = false;
      availableProblems.value = [];
      selectedProblemId.value = "";
      jobContentData.value = null;
      changesData.value = null;
      changesTab.value = "history";
      showCurrencies.value = false;
      convertValue.value = 1;
      convertFrom.value = "";
      convertTo.value = "";
      convertResult.value = null;
      createCollection.value = "";
      createFormData.value = {};
      createRefInputs.value = {};
      createdChangeRecord.value = null;
      showLogs.value = false;
      logsTab.value = "list";
      logsData.value = [];
      logsFilterType.value = "";
      logsFilterCreatedBy.value = "";
      newLogType.value = "";
      newLogCreatedBy.value = "";
      newLogCreatedByName.value = "";
      newLogData.value = "{}";
      logSaveStatus.value = null;
    }

    async function refreshDirectory() {
      const query: DirectoryQuery = {
        text: searchQuery.value.trim() || null,
        category: selectedCategory.value,
        tags: selectedTags.value,
      };
      directoryData.value = await loadDirectory(query);
    }

    async function handleDirectoryClick() {
      clearAllContent();
      await refreshDirectory();
      showSearch.value = true;
    }

    async function handlePriceListClick() {
      clearAllContent();
      priceListData.value = await loadPriceList({ formulaFactory: tnfrLegacyWithHours });
    }

    async function handleJobContentClick() {
      clearAllContent();
      showJobContent.value = true;

      // Load problems from directory
      const directoryResult = await loadDirectory({ text: null, category: null, tags: [] });
      availableProblems.value = directoryResult.problems;
    }

    async function handleGetContent() {
      if (!selectedProblemId.value) return;
      jobContentData.value = await loadJobContentByProblemId(selectedProblemId.value);
    }

    async function handleChangesClick() {
      clearAllContent();
      // Load changes and books in parallel
      const [changesResult, booksResult] = await Promise.all([
        loadChanges(),
        loadBooks(),
      ]);
      changesData.value = changesResult;
      availableBooks.value = booksResult;
    }

    async function handleDownloadChanges() {
      isDownloading.value = true;
      downloadStatus.value = null;

      try {
        const result = await downloadChanges();

        if (result.success) {
          downloadStatus.value = {
            success: true,
            message: `Downloaded ${result.downloaded}, processed ${result.processed}`,
          };
        } else {
          downloadStatus.value = {
            success: false,
            message: result.errors.join("; ") || "Download failed",
          };
        }

        // Refresh the changes list
        changesData.value = await loadChanges();
      } catch (error) {
        downloadStatus.value = {
          success: false,
          message: error instanceof Error ? error.message : String(error),
        };
      } finally {
        isDownloading.value = false;
      }
    }

    async function handleCurrenciesClick() {
      clearAllContent();
      if (!currenciesData.value) {
        currenciesData.value = await loadCurrencies();
      }
      // Load currency prefs from userPrefs table
      currencyPrefsData.value = await loadCurrencyPrefs();
      showCurrencies.value = true;
    }

    function handleConvert() {
      if (!currenciesData.value || !convertFrom.value || !convertTo.value) return;
      convertResult.value = convert(
        currenciesData.value.rates,
        convertFrom.value,
        convertTo.value,
        convertValue.value
      );
    }

    async function handleLogsClick() {
      clearAllContent();
      showLogs.value = true;
      await handleRefreshLogs();
    }

    async function handleRefreshLogs() {
      const options: { log_type?: string; created_by?: string } = {};
      if (logsFilterType.value.trim()) {
        options.log_type = logsFilterType.value.trim();
      }
      if (logsFilterCreatedBy.value.trim()) {
        options.created_by = logsFilterCreatedBy.value.trim();
      }
      logsData.value = await listLogs(options);
    }

    async function handleSaveLog() {
      logSaveStatus.value = null;

      if (!newLogType.value.trim()) {
        logSaveStatus.value = { success: false, message: "log_type is required" };
        return;
      }
      if (!newLogCreatedBy.value.trim()) {
        logSaveStatus.value = { success: false, message: "created_by is required" };
        return;
      }
      if (!newLogCreatedByName.value.trim()) {
        logSaveStatus.value = { success: false, message: "created_by_name is required" };
        return;
      }

      let logData: Record<string, any>;
      try {
        logData = JSON.parse(newLogData.value);
      } catch (e) {
        logSaveStatus.value = { success: false, message: "log_data must be valid JSON" };
        return;
      }

      try {
        const id = await saveLog({
          log_type: newLogType.value.trim(),
          log_data: logData,
          created_by: newLogCreatedBy.value.trim(),
          created_by_name: newLogCreatedByName.value.trim(),
        });
        logSaveStatus.value = { success: true, message: `Log saved with ID: ${id}` };

        // Clear form
        newLogType.value = "";
        newLogCreatedBy.value = "";
        newLogCreatedByName.value = "";
        newLogData.value = "{}";

        // Refresh list
        await handleRefreshLogs();
      } catch (e) {
        logSaveStatus.value = { success: false, message: e instanceof Error ? e.message : String(e) };
      }
    }

    const selectedSchema = computed(() => {
      if (!createCollection.value) return null;
      return collectionSchemas.find(s => s.name === createCollection.value) || null;
    });

    function handleCollectionChange() {
      // Reset form when collection changes
      createFormData.value = {};
      createRefInputs.value = {};
      createdChangeRecord.value = null;
      commitStatus.value = null;

      // Initialize form with empty values based on schema
      if (selectedSchema.value) {
        for (const field of selectedSchema.value.fields) {
          if (field.type === "ref") {
            createRefInputs.value[field.name] = "";
          } else if (field.type === "number") {
            createFormData.value[field.name] = 0;
          } else {
            createFormData.value[field.name] = "";
          }
        }
      }
    }

    async function generateChangeRecord() {
      if (!selectedSchema.value) return;

      // Reset commit status when generating new record
      commitStatus.value = null;

      const data: Record<string, any> = {};
      const refs: any[] = [];

      for (const field of selectedSchema.value.fields) {
        if (field.type === "ref") {
          // Handle ref fields - parse comma-separated refIds
          const refInput = createRefInputs.value[field.name]?.trim();
          if (refInput) {
            const refIds = refInput.split(",").map(s => s.trim()).filter(Boolean);
            if (refIds.length > 0) {
              refs.push({
                collection: field.refCollection,
                refIds: refIds,
                targetField: field.name,
              });
            }
          }
        } else {
          // Handle regular fields
          const value = createFormData.value[field.name];
          if (value !== "" && value !== null && value !== undefined) {
            if (field.type === "number") {
              // For costsMaterial.quantity, convert from preferred currency to base
              if (createCollection.value === "costsMaterial" && field.name === "quantity") {
                console.log('[generateChangeRecord] Converting quantity:', value);
                const baseValue = await convertFromPrefs(Number(value));
                console.log('[generateChangeRecord] Converted to base:', baseValue);
                if (baseValue !== null) {
                  data[field.name] = baseValue;
                } else {
                  // Fallback to raw value if no currency prefs
                  console.log('[generateChangeRecord] No prefs, using raw value');
                  data[field.name] = Number(value);
                }
              } else {
                data[field.name] = Number(value);
              }
            } else {
              data[field.name] = value;
            }
          }
        }
      }

      // Add refs array if any refs were specified
      if (refs.length > 0) {
        data.refs = refs;
      }

      createdChangeRecord.value = {
        collection: createCollection.value,
        operation: "create",
        data: data,
      };
    }

    function copyChangeRecord() {
      if (createdChangeRecord.value) {
        navigator.clipboard.writeText(JSON.stringify(createdChangeRecord.value, null, 2));
      }
    }

    async function commitChangeRecord() {
      if (!createdChangeRecord.value) {
        commitStatus.value = { success: false, message: "No change record to commit" };
        return;
      }
      if (!commitBookId.value) {
        commitStatus.value = { success: false, message: "Please select a book" };
        return;
      }

      commitStatus.value = null;
      const result = await commitChanges(
        [createdChangeRecord.value as ChangesetItem],
        { bookId: commitBookId.value }
      );

      if (result.success) {
        commitStatus.value = { success: true, message: `Committed! Record ID: ${result.recordId}` };
        // Refresh changes list
        changesData.value = await loadChanges();
      } else {
        commitStatus.value = { success: false, message: result.error || "Commit failed" };
      }
    }

    async function handleCalculatePriceClick() {
      clearAllContent();
      showCalculatePrice.value = true;

      // Load offers and currencies for the dropdowns
      const [priceListResult] = await Promise.all([
        loadPriceList({ formulaFactory: tnfrLegacyWithHours }),
        loadCurrencyData(),
      ]);
      availableOffers.value = priceListResult.offers;

      // Auto-select the first formula
      selectedFormula.value = "tnfrLegacyWithHours";
      handleFormulaChange();
    }

    function handleFormulaChange() {
      // Reset calculator state when formula changes
      showCalculator.value = false;
      showFormulaJson.value = false;
      formulaDocs.value = null;
      formulaJsonData.value = null;
      calculatorOutput.value = null;
      selectedOfferId.value = "";

      if (selectedFormula.value === "tnfrLegacyWithHours") {
        currentFormula.value = tnfrLegacyWithHours();
        calculatorInputs.value = { ...currentFormula.value.vars };
        initializeDisplayInputs();
      } else {
        currentFormula.value = null;
        calculatorInputs.value = {};
        calculatorDisplayInputs.value = {};
      }
    }

    function handleOfferSelect() {
      if (!selectedOfferId.value || !currentFormula.value) return;

      const offer = availableOffers.value.find(o => o.id === selectedOfferId.value);
      if (!offer) return;

      // Calculate materialCostBase from costsMaterial
      const materialCostBase = (offer.costsMaterial || []).reduce(
        (total: number, item: any) => total + (Number(item?.quantity) || 0),
        0
      );

      // Calculate timeCostBase from costsTime
      const timeCostBase = (offer.costsTime || []).reduce(
        (total: number, item: any) => total + (Number(item?.hours) || 0),
        0
      );

      // Update calculator inputs (set .value for VarMeta objects)
      if (calculatorInputs.value.materialCostBase) {
        calculatorInputs.value.materialCostBase.value = materialCostBase;
        // Update display input for currency field
        calculatorDisplayInputs.value.materialCostBase = convertToDisplayCurrency(materialCostBase);
      }
      if (calculatorInputs.value.timeCostBase) {
        calculatorInputs.value.timeCostBase.value = timeCostBase;
      }

      // Clear previous output
      calculatorOutput.value = null;
    }

    function handleShowDocs() {
      showCalculator.value = false;
      showFormulaJson.value = false;
      if (selectedFormula.value === "tnfrLegacyWithHours") {
        formulaDocs.value = tnfrDocs;
      }
    }

    function handleShowFormulaJson() {
      showCalculator.value = false;
      formulaDocs.value = null;
      showFormulaJson.value = true;
      if (currentFormula.value) {
        formulaJsonData.value = currentFormula.value;
      }
    }

    function handleShowCalculator() {
      formulaDocs.value = null;
      showFormulaJson.value = false;
      showCalculator.value = true;
      calculatorOutput.value = null;
    }

    async function runCalculation() {
      if (!currentFormula.value) return;

      // Create a fresh formula instance and apply input values
      const formula = selectedFormula.value === "tnfrLegacyWithHours"
        ? tnfrLegacyWithHours()
        : null;

      if (!formula) return;

      // Apply user inputs to formula vars (copy VarMeta objects)
      const vars = formula.vars as Record<string, any>;
      for (const key of Object.keys(calculatorInputs.value)) {
        if (key in vars) {
          const input = calculatorInputs.value[key];
          if (input && typeof input === 'object' && 'value' in input) {
            vars[key].value = input.value;
          } else {
            vars[key] = input;
          }
        }
      }

      // Run calculation (now async, includes currency conversion from prefs)
      const result = await calculatePrice([], [], formula);

      // Set the output directly - calculatePrice now includes converted values
      calculatorOutput.value = result;

      // Force re-render of json-node
      jsonKey.value++;
    }

    async function handleSearch() {
      // Text search clears category filter but keeps tags
      selectedCategory.value = null;
      await refreshDirectory();
    }

    async function handleClearSearch() {
      searchQuery.value = "";
      await refreshDirectory();
    }

    function getCategoryProblemCount(categoryId: string): number {
      if (!directoryData.value) return 0;
      const category = directoryData.value.categoryView.find(c => c.id === categoryId);
      return category?.problems.length || 0;
    }

    function getTotalCategoryProblems(): number {
      if (!directoryData.value) return 0;
      return directoryData.value.categoryView.reduce((sum, cat) => sum + cat.problems.length, 0);
    }

    async function handleCategoryClick(category: any) {
      // Toggle category selection; clears text search but keeps tags
      searchQuery.value = "";
      if (selectedCategory.value === category.id) {
        selectedCategory.value = null;
      } else {
        selectedCategory.value = category.id;
      }
      await refreshDirectory();
    }

    async function handleTagSelect() {
      if (selectedTagId.value && !selectedTags.value.includes(selectedTagId.value)) {
        selectedTags.value = [...selectedTags.value, selectedTagId.value];
        await refreshDirectory();
      }
      selectedTagId.value = "";
    }

    function getTagName(tagId: string): string {
      if (!directoryData.value) return tagId;
      const tag = directoryData.value.tags.find(t => t.id === tagId);
      return tag?.name || tagId;
    }

    async function removeTag(tagId: string) {
      selectedTags.value = selectedTags.value.filter(t => t !== tagId);
      await refreshDirectory();
    }

    async function handleClear() {
      // If directory is showing and there's input to clear, clear filters and refresh
      if (directoryData.value && (searchQuery.value || selectedCategory.value || selectedTags.value.length)) {
        searchQuery.value = "";
        selectedCategory.value = null;
        selectedTags.value = [];
        await refreshDirectory();
      } else {
        // Otherwise clear all content
        clearAllContent();
      }
    }

    async function handleRelogin() {
      const { pb } = await getApi();
      const email = pb.authStore.record?.email;
      if (email) {
        localStorage.setItem("relogin_email", email);
        localStorage.setItem("relogin_returnTo", "/framework");
      }
      router.push("/auth/logout");
    }

    function handleEditNavigate() {
      if (editId.value.trim()) {
        window.open(`/editor/${editId.value.trim()}`, '_blank');
      }
    }

    function onCopyToEdit(text: string) {
      editId.value = text;
    }

    // Load currencies on mount
    onMounted(() => {
      loadCurrencyData();
    });

    return {
      directoryData,
      priceListData,
      handleDirectoryClick,
      handleCalculatePriceClick,
      handlePriceListClick,
      handleRelogin,
      showSearch,
      searchQuery,
      handleSearch,
      handleClearSearch,
      getCategoryProblemCount,
      getTotalCategoryProblems,
      handleCategoryClick,
      handleTagSelect,
      getTagName,
      removeTag,
      handleClear,
      selectedCategory,
      selectedTags,
      selectedTagId,
      showCalculatePrice,
      selectedFormula,
      formulaDocs,
      handleShowDocs,
      handleShowFormulaJson,
      showFormulaJson,
      formulaJsonData,
      jsonExpandAll,
      jsonExpandMode,
      jsonKey,
      expandAllJson,
      collapseAllJson,
      handleFormulaChange,
      handleShowCalculator,
      showCalculator,
      inputSectionOpen,
      outputSectionOpen,
      calculatorInputs,
      calculatorDisplayInputs,
      calculatorOutput,
      runCalculation,
      currentCurrencySymbol,
      updateDisplayInput,
      availableOffers,
      selectedOfferId,
      handleOfferSelect,
      currenciesData,
      selectedCurrency,
      formatCurrencyValue,
      showJobContent,
      availableProblems,
      selectedProblemId,
      jobContentData,
      handleJobContentClick,
      handleGetContent,
      editId,
      handleEditNavigate,
      onCopyToEdit,
      changesData,
      handleChangesClick,
      handleDownloadChanges,
      downloadStatus,
      isDownloading,
      changesTab,
      collectionSchemas,
      createCollection,
      createFormData,
      createRefInputs,
      createdChangeRecord,
      selectedSchema,
      handleCollectionChange,
      generateChangeRecord,
      copyChangeRecord,
      commitChangeRecord,
      commitBookId,
      commitStatus,
      availableBooks,
      showCurrencies,
      handleCurrenciesClick,
      convertValue,
      convertFrom,
      convertTo,
      convertResult,
      handleConvert,
      currencyPrefsData,
      showLogs,
      logsTab,
      logsData,
      logsFilterType,
      logsFilterCreatedBy,
      newLogType,
      newLogCreatedBy,
      newLogCreatedByName,
      newLogData,
      logSaveStatus,
      handleLogsClick,
      handleRefreshLogs,
      handleSaveLog,
    };
  },
});
</script>

<style scoped>
.framework-view {
  padding: 16px;
  padding-bottom: 100px;
  font-family: system-ui, sans-serif;
  min-height: 100vh;
  overflow-y: auto;
}

.top-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.top-row-left,
.top-row-right {
  display: flex;
  gap: 8px;
}

button {
  padding: 8px 16px;
  font-size: 14px;
  cursor: pointer;
}

.top-row button {
  margin-right: 0;
}

.second-row {
  display: flex;
  justify-content: flex-end;
  margin-top: 8px;
}

.second-row-right {
  display: flex;
  gap: 8px;
  align-items: center;
}

.edit-input {
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  width: 200px;
}

.currency-select {
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid #ccc;
  background: white;
  min-width: 100px;
}

.calculate-price-row {
  margin-top: 12px;
  display: flex;
  gap: 8px;
  align-items: center;
}

.formula-select {
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid #ccc;
  background: white;
  min-width: 200px;
}

.job-content-section {
  margin-top: 12px;
}

.changes-section {
  margin-top: 12px;
}

.changes-controls {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
}

.changes-controls button {
  padding: 6px 12px;
  font-size: 13px;
  background: #f0f0f0;
  border: 1px solid #ccc;
}

.changes-controls button.active {
  background: #0066cc;
  color: white;
  border-color: #0055aa;
}

.changes-controls .download-btn {
  margin-left: auto;
  background: #28a745;
  color: white;
  border-color: #218838;
}

.changes-controls .download-btn:hover:not(:disabled) {
  background: #218838;
}

.changes-controls .download-btn:disabled {
  background: #6c757d;
  cursor: wait;
}

.download-status {
  font-size: 12px;
  padding: 4px 8px;
  border-radius: 4px;
}

.download-status.success {
  background: #d4edda;
  color: #155724;
}

.download-status.error {
  background: #f8d7da;
  color: #721c24;
}

.schemas-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.schema-card {
  background: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 6px;
  overflow: hidden;
}

.schema-header {
  padding: 10px 12px;
  background: #e9ecef;
  display: flex;
  align-items: baseline;
  gap: 12px;
}

.schema-name {
  font-weight: 600;
  font-family: monospace;
  font-size: 14px;
  color: #0066cc;
}

.schema-desc {
  font-size: 12px;
  color: #666;
}

.schema-fields {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}

.schema-fields th {
  text-align: left;
  padding: 8px 12px;
  background: #f0f0f0;
  font-weight: 600;
  color: #666;
  border-bottom: 1px solid #ddd;
}

.schema-fields td {
  padding: 6px 12px;
  border-bottom: 1px solid #eee;
}

.schema-fields tr:last-child td {
  border-bottom: none;
}

.schema-fields tr.required {
  background: #fffdf0;
}

.field-name {
  font-family: monospace;
  font-weight: 500;
}

.field-type {
  font-family: monospace;
  color: #666;
}

.ref-target {
  color: #0066cc;
  font-size: 11px;
}

.field-required {
  color: #cc6600;
  font-weight: 500;
}

.field-desc {
  color: #666;
}

/* Currencies Section Styles */
.currencies-section {
  margin-top: 12px;
}

.currencies-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: #f0f0f0;
  border-radius: 4px;
  margin-bottom: 12px;
}

.toolbar-label {
  font-family: monospace;
  font-size: 13px;
  font-weight: 600;
  color: #0066cc;
}

.toolbar-arrow {
  color: #666;
  font-size: 14px;
}

.convert-input {
  width: 100px;
  padding: 6px 8px;
  font-size: 13px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.convert-select {
  padding: 6px 8px;
  font-size: 13px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: white;
}

.convert-result {
  font-family: monospace;
  font-size: 14px;
  font-weight: 600;
  color: #28a745;
  margin-left: 8px;
}

.currencies-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.currencies-card {
  background: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 6px;
  padding: 12px;
}

.currencies-card h3 {
  margin: 0 0 12px 0;
  font-size: 14px;
  font-weight: 600;
  color: #333;
}

.currencies-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}

.currencies-table th {
  text-align: left;
  padding: 6px 8px;
  background: #e9ecef;
  font-weight: 600;
  color: #666;
}

.currencies-table td {
  padding: 6px 8px;
  border-bottom: 1px solid #eee;
}

.currencies-table tr:last-child td {
  border-bottom: none;
}

.currencies-table .mono {
  font-family: monospace;
}

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

.prefs-row {
  display: flex;
  justify-content: space-between;
  padding: 6px 0;
  border-bottom: 1px solid #eee;
}

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

.prefs-label {
  font-size: 12px;
  color: #666;
}

.prefs-value {
  font-size: 13px;
  font-weight: 500;
}

.no-prefs {
  color: #999;
  font-style: italic;
  font-size: 13px;
}

/* Create Form Styles */
.create-form {
  background: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 6px;
  padding: 16px;
}

.schema-info {
  font-size: 13px;
  color: #666;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid #e9ecef;
}

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

.form-row label {
  min-width: 140px;
  font-size: 13px;
  font-family: monospace;
  font-weight: 500;
}

.form-row label.required::after {
  content: " *";
  color: #cc6600;
}

.field-hint {
  display: block;
  font-size: 10px;
  color: #888;
  font-weight: normal;
}

.form-input {
  flex: 1;
  padding: 8px 12px;
  font-size: 13px;
  border: 1px solid #ccc;
  border-radius: 4px;
  max-width: 400px;
}

.form-input:focus {
  outline: none;
  border-color: #0066cc;
}

.input-with-hint {
  display: flex;
  align-items: center;
  gap: 8px;
}

.currency-hint {
  font-size: 11px;
  color: #666;
  background: #f0f0f0;
  padding: 4px 8px;
  border-radius: 4px;
  white-space: nowrap;
}

.collection-select {
  padding: 8px 12px;
  font-size: 13px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: white;
  min-width: 200px;
}

.form-actions {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid #e9ecef;
}

.form-actions button {
  padding: 10px 20px;
  background: #0066cc;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.form-actions button:hover {
  background: #0055aa;
}

.created-record {
  margin-top: 16px;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 4px;
  overflow: hidden;
}

.record-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 12px;
  background: #f0f0f0;
  border-bottom: 1px solid #ddd;
  font-size: 13px;
  font-weight: 600;
}

.record-header button {
  padding: 4px 12px;
  font-size: 12px;
  background: #28a745;
  color: white;
  border: none;
  border-radius: 3px;
  cursor: pointer;
}

.record-header button:hover {
  background: #218838;
}

.created-record pre {
  margin: 0;
  padding: 12px;
  font-size: 12px;
  overflow-x: auto;
  background: #fafafa;
}

.commit-section {
  display: flex;
  gap: 8px;
  padding: 12px;
  border-top: 1px solid #ddd;
  background: #f8f9fa;
}

.book-select {
  padding: 8px 12px;
  font-size: 13px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: white;
  min-width: 200px;
}

.commit-button {
  padding: 8px 16px;
  background: #0066cc;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 13px;
}

.commit-button:hover:not(:disabled) {
  background: #0055aa;
}

.commit-button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.commit-status {
  padding: 8px 12px;
  font-size: 13px;
  border-top: 1px solid #ddd;
}

.commit-status.success {
  background: #d4edda;
  color: #155724;
}

.commit-status.error {
  background: #f8d7da;
  color: #721c24;
}

.job-content-row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 12px;
}

.problem-select {
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid #ccc;
  background: white;
  min-width: 300px;
}

.formula-docs {
  margin-top: 12px;
  padding: 12px;
  background: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 4px;
}

.formula-docs pre {
  margin: 0;
  white-space: pre-wrap;
  font-size: 13px;
  line-height: 1.5;
}

.formula-json {
  margin-top: 12px;
}

.formula-json pre {
  margin: 0;
  padding: 12px;
  background: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 4px;
  max-height: 500px;
  overflow-y: auto;
}

.calculator-view {
  margin-top: 12px;
}

.collapsible-section {
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 12px;
}

.section-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: #f5f5f5;
  cursor: pointer;
  font-weight: 600;
  user-select: none;
}

.section-header:hover {
  background: #eee;
}

.collapse-icon {
  width: 16px;
  text-align: center;
  font-weight: bold;
}

.section-content {
  padding: 12px;
}

.input-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.input-row label {
  min-width: 220px;
  font-size: 13px;
  font-family: monospace;
}

.unit-label {
  color: #888;
  font-size: 11px;
}

.converted-value {
  color: #0066cc;
  font-size: 13px;
  font-family: monospace;
  margin-left: 8px;
}

.input-row input {
  padding: 6px 10px;
  font-size: 13px;
  border: 1px solid #ccc;
  border-radius: 3px;
  width: 150px;
}

.currency-symbol {
  font-size: 13px;
  font-family: monospace;
  color: #666;
  margin-right: 4px;
}

.input-row .array-value {
  font-size: 13px;
  color: #666;
  font-style: italic;
}

.run-button {
  margin-top: 12px;
  padding: 10px 20px;
  background: #0066cc;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.run-button:hover {
  background: #0055aa;
}

.output-row {
  display: flex;
  gap: 12px;
  margin-bottom: 6px;
  font-size: 13px;
}

.output-label {
  min-width: 180px;
  font-family: monospace;
  color: #666;
}

.output-value {
  font-family: monospace;
  font-weight: 600;
}

.derived-vars {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid #eee;
}

.no-output {
  color: #999;
  font-style: italic;
}

.search-container {
  margin-top: 12px;
  display: flex;
  gap: 8px;
}

.search-container input {
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid #ccc;
  flex: 1;
  max-width: 300px;
}

.filter-section {
  margin-top: 12px;
}

.filter-label {
  font-size: 12px;
  font-weight: 600;
  color: #666;
  margin-bottom: 6px;
}

.category-buttons,
.tag-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.category-btn,
.tag-btn {
  padding: 4px 10px;
  font-size: 12px;
  background: #f0f0f0;
  border: 1px solid #ccc;
}

.category-btn.selected,
.tag-btn.selected {
  background: #0066cc;
  color: white;
  border-color: #0055aa;
}

.tag-select {
  padding: 6px 10px;
  font-size: 12px;
  border: 1px solid #ccc;
  background: white;
  min-width: 180px;
}

.selected-tags {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  font-size: 12px;
  background: #e0e7ff;
  border: 1px solid #a5b4fc;
  border-radius: 12px;
}

.tag-remove {
  padding: 0;
  margin: 0;
  border: none;
  background: none;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  color: #6366f1;
}

.tag-remove:hover {
  color: #4338ca;
}

.results-info {
  margin-top: 12px;
  font-size: 13px;
  color: #666;
}

.json-block {
  margin-top: 16px;
}

.json-block pre {
  margin-top: 0;
}

.json-controls {
  display: flex;
  gap: 8px;
  margin-bottom: 8px;
}

.json-controls button {
  padding: 4px 10px;
  font-size: 12px;
  background: #f0f0f0;
  border: 1px solid #ccc;
  cursor: pointer;
}

.json-controls button:hover {
  background: #e0e0e0;
}

pre {
  margin-top: 16px;
  padding: 12px;
  background: #f5f5f5;
  border: 1px solid #ddd;
  overflow-x: auto;
}

code {
  font-family: monospace;
  font-size: 12px;
  white-space: pre;
}

/* Logs Section Styles */
.logs-section {
  margin-top: 12px;
}

.logs-controls {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
}

.logs-controls button {
  padding: 6px 12px;
  font-size: 13px;
  background: #f0f0f0;
  border: 1px solid #ccc;
}

.logs-controls button.active {
  background: #0066cc;
  color: white;
  border-color: #0055aa;
}

.logs-controls .refresh-btn {
  margin-left: auto;
  background: #28a745;
  color: white;
  border-color: #218838;
}

.logs-controls .refresh-btn:hover {
  background: #218838;
}

.logs-filters {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
}

.filter-input {
  padding: 6px 10px;
  font-size: 13px;
  border: 1px solid #ccc;
  border-radius: 4px;
  width: 180px;
}

.logs-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  background: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 6px;
  overflow: hidden;
}

.logs-table th {
  text-align: left;
  padding: 10px 12px;
  background: #e9ecef;
  font-weight: 600;
  color: #666;
  border-bottom: 1px solid #ddd;
}

.logs-table td {
  padding: 8px 12px;
  border-bottom: 1px solid #eee;
  vertical-align: top;
}

.logs-table tr:last-child td {
  border-bottom: none;
}

.logs-table .mono {
  font-family: monospace;
}

.created-by-id {
  font-size: 10px;
  color: #888;
}

.log-data-cell details {
  cursor: pointer;
}

.log-data-cell summary {
  font-size: 11px;
  color: #0066cc;
}

.log-data-cell pre {
  margin: 8px 0 0 0;
  padding: 8px;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 11px;
  max-width: 400px;
  overflow-x: auto;
}

.form-textarea {
  flex: 1;
  padding: 8px 12px;
  font-size: 13px;
  font-family: monospace;
  border: 1px solid #ccc;
  border-radius: 4px;
  max-width: 400px;
  min-height: 100px;
  resize: vertical;
}
</style>