Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Editor">
    <ion-grid :fixed="true">
      <ion-row>
        <ion-col>
          <h2 v-if="isIdMode">Record Details: {{ searchId }}</h2>
          <h2 v-else>Database Browser</h2>
        </ion-col>
      </ion-row>

      <!-- Show ID search mode -->
      <div v-if="isIdMode">
        <ion-row>
          <ion-col>
            <p>Showing record with ID: <strong>{{ searchId }}</strong></p>
            <ion-button fill="outline" @click="goBackToBrowse">
              <ion-icon :icon="arrowBack" slot="start"></ion-icon>
              Back to Browse
            </ion-button>
            <ion-button v-if="!isEditMode" fill="solid" color="primary" @click="startEditMode">
              <ion-icon :icon="pencil" slot="start"></ion-icon>
              Edit
            </ion-button>
            <ion-button v-if="!isEditMode" fill="outline" color="danger" @click="confirmDelete">
              <ion-icon :icon="trash" slot="start"></ion-icon>
              Delete
            </ion-button>
            <ion-button v-if="isEditMode" fill="solid" color="success" @click="saveChanges">
              <ion-icon :icon="checkmark" slot="start"></ion-icon>
              Save Changes
            </ion-button>
            <ion-button v-if="isEditMode" fill="outline" color="medium" @click="cancelEdit">
              <ion-icon :icon="close" slot="start"></ion-icon>
              Cancel
            </ion-button>
          </ion-col>
        </ion-row>
      </div>

      <!-- Show create mode -->
      <div v-if="isCreateMode">
        <ion-row>
          <ion-col>
            <h2>Create New Record</h2>
            <ion-button fill="outline" @click="goBackToBrowse">
              <ion-icon :icon="arrowBack" slot="start"></ion-icon>
              Back to Browse
            </ion-button>
          </ion-col>
        </ion-row>

        <ion-row v-if="!createSelectedCollection">
          <ion-col>
            <ion-item fill="outline">
              <ion-label position="stacked">Select Collection Type</ion-label>
              <select v-model="createSelectedCollection" @change="onCreateCollectionSelect" class="table-select">
                <option value="">Choose collection...</option>
                <option v-for="table in allowedTables" :key="table" :value="table">
                  {{ table }}
                </option>
              </select>
            </ion-item>
          </ion-col>
        </ion-row>

        <ion-row v-else>
          <ion-col>
            <ion-card>
              <ion-card-header>
                <ion-card-title>New {{ createSelectedCollection }} Record</ion-card-title>
              </ion-card-header>
              <ion-card-content>
                <div v-for="field in createFields" :key="field.name" class="field-row">
                  <strong>{{ field.name }}:</strong>
                  <div class="field-value">
                    <!-- Regular fields -->
                    <div v-if="!field.isRelation">
                      <ion-textarea
                        v-model="createData[field.name]"
                        :placeholder="`Enter ${field.name}...`"
                        fill="outline"
                        :rows="2"
                        :auto-grow="true"
                        class="edit-input"
                        :type="field.type === 'number' ? 'number' : 'text'"
                      ></ion-textarea>
                      <small v-if="field.isCurrency" class="currency-hint">
                        💰 Entering in {{ preferences.currency }} (will be converted to base currency)
                      </small>
                    </div>

                    <!-- Relation fields -->
                    <div v-else class="relation-edit">
                      <!-- Single relation selector -->
                      <div v-if="field.relationType === 'single'">
                        <select
                          v-model="editedRelations[field.name]"
                          class="table-select"
                          @change="onSingleRelationChange(field.name, $event)"
                        >
                          <option :value="null">None</option>
                          <option
                            v-for="option in availableRelations[field.name]"
                            :key="option.value"
                            :value="option.value"
                          >
                            {{ option.label }}
                          </option>
                        </select>
                      </div>

                      <!-- Multi relation selector -->
                      <div v-else>
                        <!-- Current relations as removable chips -->
                        <div v-if="editedRelations[field.name]?.length > 0" class="current-relations">
                          <ion-chip
                            v-for="relationId in editedRelations[field.name]"
                            :key="relationId"
                            @click="removeRelation(field.name, relationId)"
                            class="relation-chip removable"
                          >
                            {{ getRelationLabel(field.name, relationId) }}
                            <ion-icon :icon="close" class="remove-icon"></ion-icon>
                          </ion-chip>
                        </div>

                        <!-- Add new relations button -->
                        <div class="add-relations">
                          <ion-button
                            fill="outline"
                            size="small"
                            @click="openRelationSelector(field.name)"
                          >
                            Add {{ field.name }}...
                          </ion-button>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
                <div class="create-actions">
                  <ion-button fill="outline" @click="cancelCreate">
                    <ion-icon :icon="close" slot="start"></ion-icon>
                    Cancel
                  </ion-button>
                  <ion-button color="primary" @click="saveNewRecord">
                    <ion-icon :icon="checkmark" slot="start"></ion-icon>
                    Create Record
                  </ion-button>
                </div>
              </ion-card-content>
            </ion-card>
          </ion-col>
        </ion-row>
      </div>

      <!-- Show table browser mode -->
      <div v-else-if="!isIdMode">
        <ion-row class="lookup-row">
          <ion-col size="auto">
            <ion-input
              v-model="lookupId"
              placeholder="Enter record ID"
              class="lookup-input"
            ></ion-input>
          </ion-col>
          <ion-col size="auto">
            <ion-button color="medium" @click="handleLookup">
              Look up
            </ion-button>
          </ion-col>
        </ion-row>
        <ion-row>
          <ion-col>
            <ion-button color="success" @click="navigateToCreate">
              <ion-icon :icon="add" slot="start"></ion-icon>
              Create New Record
            </ion-button>
            <ion-button color="primary" @click="createNewBook">
              <ion-icon :icon="add" slot="start"></ion-icon>
              Create New Book
            </ion-button>
            <ion-button
              :fill="showChangeHistory ? 'solid' : 'outline'"
              :color="showChangeHistory ? 'tertiary' : 'tertiary'"
              @click="toggleChangeHistory"
            >
              <ion-icon :icon="listOutline" slot="start"></ion-icon>
              {{ showChangeHistory ? 'Hide Changes' : 'Review Changes' }}
            </ion-button>
          </ion-col>
        </ion-row>

        <ion-row>
          <ion-col>
            <ion-button
              :fill="showTechHandbook ? 'solid' : 'outline'"
              color="secondary"
              @click="browseTechHandbook"
            >
              {{ showTechHandbook ? 'Hide Tech Handbook' : 'Browse Tech Handbook' }}
            </ion-button>
          </ion-col>
        </ion-row>

        <!-- Tech Handbook Browser -->
        <div v-if="showTechHandbook" class="tech-handbook-section">
          <ion-card>
            <ion-card-header>
              <ion-card-title>Tech Handbook Browser</ion-card-title>
            </ion-card-header>
            <ion-card-content>
              <div v-if="techHandbookLoading" class="loading-state">
                <ion-spinner name="crescent"></ion-spinner>
                <p>Loading tech handbook data...</p>
              </div>
              <div v-else-if="techHandbookError" class="error-state">
                <p>{{ techHandbookError }}</p>
              </div>
              <div v-else-if="techHandbookData.length === 0" class="empty-state">
                <p>No tech handbook entries found.</p>
              </div>
              <div v-else>
                <p class="tech-handbook-summary">
                  <strong>{{ techHandbookData.length }}</strong> offers with tech handbook entries
                </p>
                <div v-for="offer in techHandbookData" :key="offer.offerId" class="tech-handbook-offer">
                  <h3 class="offer-title">
                    <a :href="'/editor/offers/' + offer.offerId" target="_blank">{{ offer.offerName }}</a>
                  </h3>
                  <ul class="tech-handbook-items">
                    <li v-for="item in offer.items" :key="item.id" class="tech-handbook-item">
                      {{ item.content }}
                    </li>
                  </ul>
                </div>
              </div>
            </ion-card-content>
          </ion-card>
        </div>

        <ion-row v-if="!showChangeHistory">
          <ion-col size="6">
            <ion-item fill="outline">
              <ion-label position="stacked">SELECT * FROM</ion-label>
              <ion-input
                v-model="tableName"
                placeholder="Enter table name..."
                @keyup.enter="executeQuery"
              ></ion-input>
            </ion-item>
          </ion-col>
          <ion-col size="6">
            <ion-item fill="outline">
              <ion-label position="stacked">Or select from allowed tables:</ion-label>
              <select v-model="selectedTable" @change="onTableSelect" class="table-select">
                <option value="">Choose table...</option>
                <option v-for="table in allowedTables" :key="table" :value="table">
                  {{ table }}
                </option>
              </select>
            </ion-item>
          </ion-col>
        </ion-row>

        <ion-row v-if="!showChangeHistory">
          <ion-col>
            <ion-button @click="executeQuery" :disabled="!tableName.trim()">
              Execute Query
            </ion-button>
            <ion-button fill="clear" @click="clearResults">
              Clear
            </ion-button>
          </ion-col>
        </ion-row>

        <!-- Change History View -->
        <div v-if="showChangeHistory" class="change-history-section">
          <ion-row>
            <ion-col>
              <ion-card>
                <ion-card-header>
                  <ion-card-title>Change History</ion-card-title>
                  <ion-segment v-model="changeHistoryViewMode" class="view-mode-segment">
                    <ion-segment-button value="friendly">
                      <ion-label>Friendly View</ion-label>
                    </ion-segment-button>
                    <ion-segment-button value="json">
                      <ion-label>JSON Editor</ion-label>
                    </ion-segment-button>
                  </ion-segment>
                </ion-card-header>
                <ion-card-content>
                  <!-- Friendly View -->
                  <div v-if="changeHistoryViewMode === 'friendly'" class="friendly-view">
                    <div v-if="changeMessages.length === 0" class="empty-state">
                      <p>No change messages found</p>
                    </div>
                    <ion-card v-for="msg in changeMessages" :key="msg.id" class="change-message-card">
                      <ion-card-header>
                        <div class="change-header-row">
                          <ion-card-subtitle>
                            {{ formatDate(msg.created) }}
                          </ion-card-subtitle>
                          <ion-chip :color="getStatusColor(msg.status)" size="small">{{ msg.status }}</ion-chip>
                        </div>
                        <div class="change-meta">
                          <span class="meta-item">Book: {{ getBookName(msg.book) }}</span>
                        </div>
                      </ion-card-header>
                      <ion-card-content>
                        <div v-if="msg.error_message" class="error-message">
                          {{ msg.error_message }}
                        </div>
                        <div class="changeset-details">
                          <div v-for="(change, i) in msg.changeset" :key="i" class="change-item">
                            <div class="change-operation" :class="change.operation">
                              <span class="op-badge">{{ change.operation.toUpperCase() }}</span>
                              <span class="collection-name">{{ change.collection }}</span>
                              <span v-if="change.data?.refId" class="ref-id">{{ change.data.refId }}</span>
                            </div>
                            <div class="change-data-preview">
                              <div v-for="(value, key) in getPreviewData(change.data)" :key="key" class="data-preview-field">
                                <span class="field-key">{{ key }}:</span>
                                <span class="field-value-preview">{{ truncateValue(value) }}</span>
                              </div>
                            </div>
                            <ion-button fill="clear" size="small" @click="expandChange(msg, i)">
                              View Details
                            </ion-button>
                          </div>
                        </div>
                      </ion-card-content>
                    </ion-card>
                  </div>

                  <!-- JSON Editor View -->
                  <div v-else class="json-view">
                    <div class="json-controls">
                      <ion-button
                        v-if="superSecretMode"
                        color="dark"
                        size="small"
                        @click="showDeleteDbConfirmDialog = true"
                      >
                        <ion-icon :icon="skullOutline" slot="start"></ion-icon>
                        Delete Database
                      </ion-button>
                      <ion-button
                        v-if="superSecretMode"
                        color="medium"
                        size="small"
                        @click="openDbPicker"
                      >
                        <ion-icon :icon="folderOpenOutline" slot="start"></ion-icon>
                        DB File Picker
                      </ion-button>
                      <ion-button
                        v-if="superSecretMode"
                        color="success"
                        size="small"
                        @click="saveAsDefaultDb"
                      >
                        <ion-icon :icon="saveOutline" slot="start"></ion-icon>
                        Save as Default
                      </ion-button>
                      <ion-button
                        v-if="superSecretMode"
                        color="tertiary"
                        size="small"
                        @click="uploadDefaultDbToServer"
                      >
                        <ion-icon :icon="cloudUploadOutline" slot="start"></ion-icon>
                        Upload Default DB
                      </ion-button>
                      <ion-button
                        v-if="superSecretMode"
                        color="secondary"
                        size="small"
                        @click="downloadDefaultDbFromServer"
                      >
                        <ion-icon :icon="cloudDownloadOutline" slot="start"></ion-icon>
                        Download Default
                      </ion-button>
                      <ion-button color="danger" size="small" @click="showReplayConfirmDialog = true">
                        <ion-icon :icon="refreshOutline" slot="start"></ion-icon>
                        Replay All Changes
                      </ion-button>
                      <ion-button fill="outline" size="small" @click="copyAllJson">
                        <ion-icon :icon="copyOutline" slot="start"></ion-icon>
                        Copy All
                      </ion-button>
                      <ion-button color="warning" size="small" @click="showCombineChangesDialog">
                        <ion-icon :icon="gitMergeOutline" slot="start"></ion-icon>
                        Combine Changes
                      </ion-button>
                      <ion-button color="success" size="small" @click="pushChangesToServer">
                        <ion-icon :icon="cloudUploadOutline" slot="start"></ion-icon>
                        Push Changes
                      </ion-button>
                      <ion-button color="primary" size="small" @click="downloadAndViewAllChanges">
                        <ion-icon :icon="cloudDownloadOutline" slot="start"></ion-icon>
                        Download All Changes
                      </ion-button>
                    </div>
                    <div v-if="changeMessages.length === 0" class="empty-state">
                      <p>No change messages found</p>
                    </div>
                    <div v-for="(msg, msgIndex) in changeMessages" :key="msg.id" class="json-message-block">
                      <div class="json-message-header">
                        <span class="json-date">{{ formatDate(msg.created) }}</span>
                        <ion-chip
                          v-if="!jsonEdited[msgIndex]"
                          :color="getStatusColor(msg.status)"
                          size="small"
                        >{{ msg.status }}</ion-chip>
                        <ion-button
                          v-else
                          size="small"
                          color="warning"
                          @click="saveChangeset(msgIndex)"
                          :disabled="!!jsonErrors[msgIndex]"
                        >
                          Save
                        </ion-button>
                        <ion-button fill="clear" size="small" @click="copyMessageJson(msg)">
                          <ion-icon :icon="copyOutline"></ion-icon>
                        </ion-button>
                        <ion-button fill="clear" size="small" color="danger" @click="deleteChangesetLocally(msg.id)">
                          <ion-icon :icon="trash"></ion-icon>
                        </ion-button>
                      </div>
                      <textarea
                        class="json-editor"
                        :value="formatJsonForEdit(msg)"
                        @input="(e) => updateChangesetJson(msgIndex, e)"
                        spellcheck="false"
                      ></textarea>
                      <div v-if="jsonErrors[msgIndex]" class="json-error">
                        {{ jsonErrors[msgIndex] }}
                      </div>
                    </div>
                  </div>
                </ion-card-content>
              </ion-card>
            </ion-col>
          </ion-row>
        </div>
      </div>

      <ion-row v-if="loading">
        <ion-col>
          <ion-spinner></ion-spinner>
          <p>Executing query...</p>
        </ion-col>
      </ion-row>

      <ion-row v-if="error">
        <ion-col>
          <ion-card color="danger">
            <ion-card-header>
              <ion-card-title>Error</ion-card-title>
            </ion-card-header>
            <ion-card-content>
              <pre>{{ error }}</pre>
            </ion-card-content>
          </ion-card>
        </ion-col>
      </ion-row>

      <ion-row v-if="results">
        <ion-col>
          <ion-card>
            <ion-card-header>
              <ion-card-title>Results from: {{ lastQuery }}</ion-card-title>
              <ion-card-subtitle v-if="results.values">
                <span v-if="!isIdMode && resultsFilter.trim()">
                  {{ filteredResults.length }} of {{ results.values.length }} row(s) shown
                </span>
                <span v-else>
                  {{ results.values.length }} row(s) returned
                </span>
              </ion-card-subtitle>
            </ion-card-header>
            <ion-card-content>
              <!-- Filter input - only show in browse mode, not in ID/edit mode -->
              <div v-if="!isIdMode && results.values && results.values.length > 0" class="filter-container">
                <ion-item fill="outline" lines="none">
                  <ion-label position="stacked">Filter results:</ion-label>
                  <ion-input
                    v-model="resultsFilter"
                    placeholder="Type to filter..."
                    clearInput
                  ></ion-input>
                </ion-item>
              </div>

              <div v-if="filteredResults && filteredResults.length > 0" class="results-cards">
                <ion-card v-for="(row, index) in filteredResults" :key="index" class="record-card">
                  <ion-card-header>
                    <ion-card-subtitle>Record {{ index + 1 }}</ion-card-subtitle>
                  </ion-card-header>
                  <ion-card-content>
                    <div v-for="column in Object.keys(row).filter(col => !shouldHideField(col))" :key="column" class="field-row">
                      <strong>{{ column }}:</strong>
                      <div class="field-value">
                        <!-- Edit mode for book field (single relation) -->
                        <div v-if="isEditMode && column === 'book' && !isSystemField(column)">
                          <select
                            v-model="editedData[column]"
                            class="table-select"
                          >
                            <option v-for="book in booksCache" :key="book.id" :value="book.id">
                              {{ book.name }}
                            </option>
                          </select>
                        </div>
                        <!-- Edit mode for simple fields -->
                        <div v-else-if="isEditMode && !Array.isArray(row[column]) && !isSystemField(column) && column !== 'book'">
                          <ion-textarea
                            v-model="editedData[column]"
                            :placeholder="formatValue(row[column])"
                            fill="outline"
                            :rows="2"
                            :auto-grow="true"
                            class="edit-input"
                            :type="isCurrencyField(column) ? 'number' : 'text'"
                          ></ion-textarea>
                          <small v-if="isCurrencyField(column)" class="currency-hint">
                            💰 Editing in {{ preferences.currency }} (converted from base currency)
                          </small>
                        </div>
                        <!-- Handle relation data - editable in edit mode -->
                        <div v-else-if="Array.isArray(row[column]) && typeof row[column][0] === 'object'" class="relation-data">
                          <!-- Edit mode for relations -->
                          <div v-if="isEditMode" class="relation-edit">
                            <div class="relation-edit-header">{{ column }} Relations:</div>

                            <!-- Current relations as removable chips -->
                            <div v-if="editedRelations[column]?.length > 0" class="current-relations">
                              <ion-chip
                                v-for="relationId in editedRelations[column]"
                                :key="relationId"
                                @click="removeRelation(column, relationId)"
                                class="relation-chip removable"
                              >
                                {{ getRelationLabel(column, relationId) }}
                                <ion-icon :icon="close" class="remove-icon"></ion-icon>
                              </ion-chip>
                            </div>

                            <!-- Add new relations -->
                            <div class="add-relations">
                              <ion-button
                                fill="outline"
                                @click="openRelationSelector(column)"
                                class="relation-add-button"
                              >
                                Add {{ column }}...
                              </ion-button>
                            </div>
                          </div>

                          <!-- Read-only view -->
                          <div v-else>
                            <div v-for="(relatedItem, relIndex) in row[column]" :key="relIndex" class="related-item">
                              <div class="related-header">{{ column }} #{{ relIndex + 1 }}</div>
                              <div v-for="(relValue, relKey) in Object.fromEntries(Object.entries(relatedItem).filter(([key]) => !shouldHideField(key)))" :key="relKey" class="related-field">
                                <span class="related-key">{{ relKey }}:</span>
                                <span class="related-value">
                                  <ShowCurrency v-if="relKey === 'quantity' && typeof relValue === 'number'" :currencyIn="relValue" />
                                  <a v-else-if="typeof relKey === 'string' && isIdField(relKey) && relValue" @click="navigateToId(typeof relValue === 'string' ? relValue : String(relValue))" class="id-link">{{ relValue }}</a>
                                  <span v-else>{{ relValue }}</span>
                                </span>
                              </div>
                            </div>
                          </div>
                        </div>
                        <!-- Handle regular data (read-only) -->
                        <ShowCurrency v-else-if="column === 'quantity' && typeof row[column] === 'number'" :currencyIn="row[column]" />
                        <a v-else-if="isIdField(column) && row[column]" @click="navigateToId(typeof row[column] === 'string' ? row[column] : String(row[column]))" class="id-link">{{ row[column] }}</a>
                        <span v-else-if="isBookField(column)">{{ formatBookValue(row[column]) }}</span>
                        <span v-else>{{ formatValue(row[column]) }}</span>
                      </div>
                    </div>
                  </ion-card-content>
                </ion-card>
              </div>
              <div v-else>
                <p>No results found.</p>
              </div>
            </ion-card-content>
          </ion-card>
        </ion-col>
      </ion-row>

      <!-- Single Offer Tech Handbook (shown when viewing /editor/offers/:id) -->
      <ion-row v-if="isIdMode && selectedTable === 'offers'">
        <ion-col>
          <ion-card>
            <ion-card-header>
              <ion-card-title>Tech Handbook</ion-card-title>
            </ion-card-header>
            <ion-card-content>
              <div v-if="singleOfferTechHandbookLoading" class="loading-state">
                <ion-spinner name="crescent"></ion-spinner>
                <p>Loading tech handbook...</p>
              </div>
              <div v-else-if="singleOfferTechHandbookError" class="error-state">
                <p>{{ singleOfferTechHandbookError }}</p>
              </div>
              <div v-else-if="!singleOfferTechHandbook || singleOfferTechHandbook.items.length === 0" class="empty-state">
                <p>No tech handbook entries for this offer.</p>
              </div>
              <div v-else>
                <p class="tech-handbook-summary">
                  <strong>{{ singleOfferTechHandbook.items.length }}</strong> tech handbook item(s)
                </p>
                <ul class="tech-handbook-items">
                  <li v-for="item in singleOfferTechHandbook.items" :key="item.id" class="tech-handbook-item">
                    {{ item.content }}
                  </li>
                </ul>
              </div>
            </ion-card-content>
          </ion-card>
        </ion-col>
      </ion-row>
    </ion-grid>

    <!-- Changeset Preview Dialog -->
    <ion-modal :is-open="showChangesetDialog" @did-dismiss="showChangesetDialog = false">
      <ion-header>
        <ion-toolbar>
          <ion-title>Proposed Changeset</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="showChangesetDialog = false">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding changeset-modal-content">
        <div v-if="proposedChangeset && proposedChangeset.changes && proposedChangeset.changes.length > 0">
          <h3>Proposed Changeset</h3>
          <p>This changeset will be submitted to the server and applied during sync.</p>
          <p v-if="proposedChangeset.bookName" class="book-info">
            <strong>Book:</strong> {{ proposedChangeset.bookName }}
          </p>

          <ion-card v-for="(changeItem, index) in proposedChangeset.changes" :key="index" class="changeset-card">
            <ion-card-header>
              <ion-card-title>{{ changeItem.operation.toUpperCase() }} - {{ changeItem.collection }}</ion-card-title>
            </ion-card-header>
            <ion-card-content>
              <div class="changeset-data">
                <h4>Data:</h4>
                <div v-for="(value, key) in changeItem.data" :key="key" class="data-field">
                  <strong>{{ key }}:</strong>
                  <span class="data-value">{{ formatValue(value) }}</span>
                </div>
              </div>
            </ion-card-content>
          </ion-card>
        </div>
      </ion-content>

      <!-- Fixed Footer with Action Buttons -->
      <ion-footer>
        <ion-toolbar>
          <div class="changeset-actions">
            <ion-button fill="outline" @click="showChangesetDialog = false">Cancel</ion-button>
            <ion-button color="primary" @click="submitChangeset" :disabled="loading">
              <ion-spinner v-if="loading" name="crescent" slot="start"></ion-spinner>
              {{ loading ? 'Submitting...' : 'Submit Changeset' }}
            </ion-button>
          </div>
        </ion-toolbar>
      </ion-footer>
    </ion-modal>

    <!-- Delete Confirmation Dialog -->
    <ion-modal :is-open="showDeleteDialog" @did-dismiss="showDeleteDialog = false">
      <ion-header>
        <ion-toolbar>
          <ion-title>Confirm Delete</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="showDeleteDialog = false">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div v-if="proposedChangeset && proposedChangeset.isDelete">
          <h3>Delete Record</h3>
          <p>Are you sure you want to delete this record?</p>

          <ion-card class="warning-card">
            <ion-card-content>
              <p><strong>Collection:</strong> {{ proposedChangeset.changes[0].collection }}</p>
              <p v-if="proposedChangeset.bookName"><strong>Book:</strong> {{ proposedChangeset.bookName }}</p>
              <p v-if="proposedChangeset.changes[0].data.refId">
                <strong>Reference ID:</strong> {{ proposedChangeset.changes[0].data.refId }}
              </p>
              <p class="warning-text">
                <ion-icon :icon="alertCircle" color="warning"></ion-icon>
                This action cannot be undone!
              </p>
            </ion-card-content>
          </ion-card>

          <div class="dialog-actions">
            <ion-button fill="outline" @click="showDeleteDialog = false">Cancel</ion-button>
            <ion-button color="danger" @click="submitDelete">
              <ion-icon :icon="trash" slot="start"></ion-icon>
              Delete Record
            </ion-button>
          </div>
        </div>
      </ion-content>
    </ion-modal>

    <!-- Relation Selector Modal -->
    <ion-modal :is-open="showRelationSelector" @did-dismiss="closeRelationSelector">
      <ion-header>
        <ion-toolbar>
          <ion-title>Select {{ currentRelationField }}</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="closeRelationSelector">Cancel</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding relation-modal-content">
        <!-- Search/Filter Input -->
        <ion-input
          v-model="relationSearchText"
          :placeholder="`Search ${currentRelationField}...`"
          fill="outline"
          class="relation-search-input"
        ></ion-input>

        <!-- Available Relations List -->
        <div class="relation-options">
          <ion-item
            v-for="option in getFilteredRelations(currentRelationField)"
            :key="option.value"
            button
            @click="toggleRelationSelection(option.value)"
            class="relation-option-item"
          >
            <ion-checkbox
              :checked="selectedRelationsToAdd.includes(option.value)"
              slot="start"
            ></ion-checkbox>
            <ion-label>{{ option.label }}</ion-label>
          </ion-item>

          <div v-if="getFilteredRelations(currentRelationField).length === 0" class="no-results">
            <p>No {{ currentRelationField }} found matching "{{ relationSearchText }}"</p>
          </div>
        </div>
      </ion-content>

      <!-- Fixed Footer with Action Buttons -->
      <ion-footer>
        <ion-toolbar>
          <div class="relation-selector-actions">
            <ion-button fill="outline" @click="closeRelationSelector">Cancel</ion-button>
            <ion-button
              color="primary"
              @click="confirmRelationSelection"
              :disabled="selectedRelationsToAdd.length === 0"
            >
              Add Selected ({{ selectedRelationsToAdd.length }})
            </ion-button>
          </div>
        </ion-toolbar>
      </ion-footer>
    </ion-modal>

    <!-- Change Detail Modal -->
    <ion-modal :is-open="showChangeDetailModal" @did-dismiss="showChangeDetailModal = false">
      <ion-header>
        <ion-toolbar>
          <ion-title>Change Details</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="showChangeDetailModal = false">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div v-if="selectedChangeDetail">
          <div class="detail-header">
            <span class="op-badge" :class="selectedChangeDetail.operation">
              {{ selectedChangeDetail.operation.toUpperCase() }}
            </span>
            <span class="collection-name">{{ selectedChangeDetail.collection }}</span>
          </div>
          <div class="detail-data">
            <h4>Data</h4>
            <div v-for="(value, key) in selectedChangeDetail.data" :key="key" class="detail-field">
              <strong>{{ key }}:</strong>
              <pre class="detail-value">{{ formatValue(value) }}</pre>
            </div>
          </div>
        </div>
      </ion-content>
    </ion-modal>

    <!-- Save Changeset Confirmation Dialog -->
    <ion-modal :is-open="showSaveConfirmDialog" @did-dismiss="cancelSaveChangeset">
      <ion-header>
        <ion-toolbar color="warning">
          <ion-title>Confirm Edit</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="cancelSaveChangeset">Cancel</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div class="confirm-dialog-content">
          <ion-icon :icon="alertCircle" class="confirm-icon" color="warning"></ion-icon>
          <h3>Update Change Record?</h3>
          <p>You are about to modify the change history in the local database.</p>
          <p class="confirm-note">
            This edit will be applied when the pricebook data is rebuilt from the change log.
            Make sure your JSON changes are correct before proceeding.
          </p>
          <div v-if="pendingSaveIndex !== null && changeMessages[pendingSaveIndex]" class="confirm-details">
            <strong>Record:</strong> {{ changeMessages[pendingSaveIndex].id }}<br>
            <strong>Created:</strong> {{ formatDate(changeMessages[pendingSaveIndex].created) }}<br>
            <strong>Book:</strong> {{ getBookName(changeMessages[pendingSaveIndex].book) }}
          </div>
        </div>
      </ion-content>
      <ion-footer>
        <ion-toolbar>
          <div class="confirm-actions">
            <ion-button fill="outline" @click="cancelSaveChangeset">Cancel</ion-button>
            <ion-button color="warning" @click="confirmSaveChangeset">
              <ion-icon :icon="checkmark" slot="start"></ion-icon>
              Save Changes
            </ion-button>
          </div>
        </ion-toolbar>
      </ion-footer>
    </ion-modal>

    <!-- Replay Confirmation Dialog -->
    <ion-modal :is-open="showReplayConfirmDialog" @did-dismiss="cancelReplay">
      <ion-header>
        <ion-toolbar color="danger">
          <ion-title>Replay All Changes</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="cancelReplay">Cancel</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div class="confirm-dialog-content">
          <ion-icon :icon="refreshOutline" class="confirm-icon" color="danger"></ion-icon>
          <h3>Rebuild Database from Change Log?</h3>
          <p>This will perform the following actions:</p>
          <ol class="replay-steps">
            <li>Clear all pricebook data (menus, offers, tiers, costs, etc.)</li>
            <li>Replay all {{ changeMessages.length }} change messages in chronological order</li>
            <li>Update each message's status based on replay result</li>
          </ol>
          <div class="replay-warning">
            <ion-icon :icon="alertCircle" color="danger"></ion-icon>
            <strong>Warning:</strong> This is a destructive operation. All current pricebook data will be deleted and rebuilt from the change log. Make sure you have saved any edits to the change messages before proceeding.
          </div>
        </div>
      </ion-content>
      <ion-footer>
        <ion-toolbar>
          <div class="confirm-actions">
            <ion-button fill="outline" @click="cancelReplay">Cancel</ion-button>
            <ion-button color="danger" @click="confirmReplay">
              <ion-icon :icon="refreshOutline" slot="start"></ion-icon>
              Replay All Changes
            </ion-button>
          </div>
        </ion-toolbar>
      </ion-footer>
    </ion-modal>

    <!-- Replay Progress Overlay -->
    <div v-if="replayProgress" class="replay-progress-overlay">
      <ion-card class="replay-progress-card">
        <ion-card-content>
          <ion-spinner name="crescent"></ion-spinner>
          <h3>Replaying Changes...</h3>
          <p>{{ replayProgress.status }}</p>
          <div class="progress-bar">
            <div
              class="progress-fill"
              :style="{ width: `${(replayProgress.current / replayProgress.total) * 100}%` }"
            ></div>
          </div>
          <p class="progress-count">{{ replayProgress.current }} / {{ replayProgress.total }}</p>
        </ion-card-content>
      </ion-card>
    </div>

    <!-- Replay Results Dialog -->
    <ion-modal :is-open="showReplayResultsDialog" @did-dismiss="showReplayResultsDialog = false">
      <ion-header>
        <ion-toolbar :color="replayResults.errors.length > 0 ? 'warning' : 'success'">
          <ion-title>Replay Results</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="showReplayResultsDialog = false">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div class="replay-results-summary">
          <div class="result-stat success">
            <span class="stat-number">{{ replayResults.success }}</span>
            <span class="stat-label">Succeeded</span>
          </div>
          <div class="result-stat error">
            <span class="stat-number">{{ replayResults.errors.length }}</span>
            <span class="stat-label">Failed</span>
          </div>
        </div>

        <div v-if="replayResults.errors.length > 0" class="replay-errors-section">
          <h3>Error Log</h3>
          <p class="errors-hint">The following changes failed to apply. You can edit them in the JSON view and replay again.</p>

          <div v-for="err in replayResults.errors" :key="err.id" class="replay-error-item">
            <div class="error-header">
              <span class="error-id">ID: {{ err.id }}</span>
              <span class="error-date">{{ formatDate(err.created) }}</span>
              <span class="error-book">Book: {{ getBookName(err.book) }}</span>
            </div>
            <div class="error-message">
              <ion-icon :icon="alertCircle" color="danger"></ion-icon>
              {{ err.error }}
            </div>
            <div class="error-changeset">
              <strong>Changeset:</strong>
              <pre>{{ JSON.stringify(err.changeset, null, 2) }}</pre>
            </div>
          </div>
        </div>
      </ion-content>
      <ion-footer>
        <ion-toolbar>
          <div class="confirm-actions">
            <ion-button fill="outline" @click="copyReplayLog">
              <ion-icon :icon="copyOutline" slot="start"></ion-icon>
              Copy Log
            </ion-button>
            <ion-button color="primary" @click="showReplayResultsDialog = false">
              Done
            </ion-button>
          </div>
        </ion-toolbar>
      </ion-footer>
    </ion-modal>

    <!-- Delete Database Confirmation Dialog -->
    <ion-modal :is-open="showDeleteDbConfirmDialog" @did-dismiss="showDeleteDbConfirmDialog = false">
      <ion-header>
        <ion-toolbar color="dark">
          <ion-title>Delete Database</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="showDeleteDbConfirmDialog = false">Cancel</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div class="confirm-dialog-content">
          <ion-icon :icon="skullOutline" class="confirm-icon skull-icon"></ion-icon>
          <h3>Restore from Default</h3>
          <p class="db-filename">Delete: <code>pricebookPlatformSQLite.db</code></p>
          <p class="db-filename">Restore from: <code>default.sql</code></p>
          <p>This will replace the current database with the default database.</p>
          <ol class="replay-steps">
            <li>Delete current database</li>
            <li>Copy default.sql to pricebookPlatformSQLite.db</li>
            <li>Reload the application</li>
          </ol>
          <div class="replay-warning">
            <ion-icon :icon="alertCircle" color="danger"></ion-icon>
            <strong>Warning:</strong> All local changes since the default was saved will be lost. Make sure you have saved a default database first using "Save as Default".
          </div>
        </div>
      </ion-content>
      <ion-footer>
        <ion-toolbar>
          <div class="confirm-actions">
            <ion-button fill="outline" @click="showDeleteDbConfirmDialog = false">Cancel</ion-button>
            <ion-button color="dark" @click="confirmDeleteDatabase">
              <ion-icon :icon="skullOutline" slot="start"></ion-icon>
              Restore Default
            </ion-button>
          </div>
        </ion-toolbar>
      </ion-footer>
    </ion-modal>
    <!-- Delete Database Progress Overlay -->
    <div v-if="deleteDbProgress" class="replay-progress-overlay">
      <ion-card class="replay-progress-card">
        <ion-card-content>
          <ion-spinner name="crescent"></ion-spinner>
          <h3>Resetting Database...</h3>
          <p>{{ deleteDbProgress.status }}</p>
        </ion-card-content>
      </ion-card>
    </div>

    <!-- DB File Picker Modal -->
    <ion-modal :is-open="showDbPickerDialog" @did-dismiss="showDbPickerDialog = false">
      <ion-header>
        <ion-toolbar>
          <ion-title>Database Browser</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="showDbPickerDialog = false">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div class="db-picker-content">
          <div v-if="loadingDatabases" class="db-loading">
            <ion-spinner name="crescent"></ion-spinner>
            <p>Loading databases...</p>
          </div>

          <template v-else>
            <!-- SQLite Databases Section -->
            <div class="db-section">
              <h3 class="db-section-title">SQLite Databases</h3>
              <p class="db-section-info">
                These are the SQLite databases stored inside jeepSqliteStore.
              </p>

              <div v-if="sqliteDatabases.length === 0" class="db-empty-inline">
                <p>No SQLite databases found.</p>
              </div>

              <div v-else class="db-list">
                <div
                  v-for="dbName in sqliteDatabases"
                  :key="dbName"
                  class="db-item sqlite-item"
                >
                  <div class="db-item-info">
                    <ion-icon :icon="serverOutline" class="db-icon sqlite-icon"></ion-icon>
                    <div class="db-details">
                      <span class="db-name">{{ dbName }}</span>
                      <span class="db-type">SQLite Database</span>
                    </div>
                  </div>
                </div>
              </div>
            </div>

            <!-- IndexedDB Section -->
            <div class="db-section">
              <h3 class="db-section-title">IndexedDB Stores</h3>
              <p class="db-section-info">
                Raw IndexedDB storage used by the SQLite web implementation.
              </p>

              <div v-if="availableDatabases.length === 0" class="db-empty-inline">
                <p>No IndexedDB databases found.</p>
              </div>

              <div v-else class="db-list">
                <div
                  v-for="db in availableDatabases"
                  :key="db.name"
                  class="db-item"
                >
                  <div class="db-item-info">
                    <ion-icon :icon="folderOpenOutline" class="db-icon"></ion-icon>
                    <div class="db-details">
                      <span class="db-name">{{ db.name }}</span>
                      <span v-if="db.version" class="db-version">Version: {{ db.version }}</span>
                    </div>
                  </div>
                  <div class="db-item-actions">
                    <ion-button
                      size="small"
                      fill="outline"
                      color="danger"
                      @click="deleteSpecificDb(db.name)"
                    >
                      Delete
                    </ion-button>
                  </div>
                </div>
              </div>
            </div>
          </template>

          <div class="db-picker-actions">
            <ion-button fill="outline" @click="refreshDatabaseList">
              <ion-icon :icon="refreshOutline" slot="start"></ion-icon>
              Refresh List
            </ion-button>
          </div>
        </div>
      </ion-content>
    </ion-modal>

    <!-- Download All Changes Modal -->
    <ion-modal :is-open="showDownloadedChangesModal" @did-dismiss="showDownloadedChangesModal = false">
      <ion-header>
        <ion-toolbar>
          <ion-title>All Changes from API</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="showDownloadedChangesModal = false">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div v-if="downloadedChangesLoading" class="loading-state">
          <ion-spinner name="crescent"></ion-spinner>
          <p>Downloading changes from API...</p>
        </div>
        <div v-else-if="downloadedChangesError" class="error-state">
          <p>{{ downloadedChangesError }}</p>
        </div>
        <div v-else>
          <p class="changes-summary">
            <strong>{{ downloadedChanges.length }}</strong> changesets downloaded
          </p>
          <div class="downloaded-changes-controls">
            <ion-button size="small" @click="copyAllDownloadedChanges">
              <ion-icon :icon="copyOutline" slot="start"></ion-icon>
              Copy All to Clipboard
            </ion-button>
          </div>
          <pre class="downloaded-changes-code">{{ JSON.stringify(downloadedChanges, null, 2) }}</pre>
        </div>
      </ion-content>
    </ion-modal>
  </BaseLayout>
</template>

<script setup lang="ts">
import { ref, onMounted, watch, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
  IonGrid,
  IonRow,
  IonCol,
  IonItem,
  IonLabel,
  IonInput,
  IonTextarea,
  IonButton,
  IonCard,
  IonCardHeader,
  IonCardTitle,
  IonCardSubtitle,
  IonCardContent,
  IonSpinner,
  IonIcon,
  IonModal,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonButtons,
  IonContent,
  IonFooter,
  IonChip,
  IonCheckbox,
  IonSegment,
  IonSegmentButton,
} from "@ionic/vue";
import { arrowBack, pencil, checkmark, close, add, trash, alertCircle, listOutline, copyOutline, refreshOutline, skullOutline, folderOpenOutline, serverOutline, saveOutline, cloudUploadOutline, cloudDownloadOutline, gitMergeOutline } from "ionicons/icons";
import BaseLayout from "@/components/BaseLayout.vue";
import ShowCurrency from "@/components/ShowCurrency.vue";
import { getSQLite } from "@/dataAccess/getSQLite";
import { getApi } from "@/dataAccess/getApi";
import { getDb } from "@/dataAccess/getDb";
import {
  convertToBaseCurrency,
  convertFromBaseCurrency,
} from "@/utils/currencyConverter";
import { useCurrencyStore } from "@/stores/currency";
import { usePreferencesStore } from "@/stores/preferences";
import { useSessionStore } from "@/stores/session";
import { ChangesetProcessor } from "@/dataAccess/changeset-processor";
import { clearAllIndexedDB } from "@/dataAccess/deleteDb";

const route = useRoute();
const router = useRouter();
const currencyStore = useCurrencyStore();
const preferences = usePreferencesStore();
const sessionStore = useSessionStore();
const tableName = ref("");
const selectedTable = ref("");
const results = ref<any>(null);
const error = ref("");
const loading = ref(false);
const lastQuery = ref("");
const searchId = ref("");
const isIdMode = ref(false);
const isEditMode = ref(false);
const editedData = ref<any>({});
const originalData = ref<any>({});
const showChangesetDialog = ref(false);
const proposedChangeset = ref<any>(null);
const showDeleteDialog = ref(false);
const editedRelations = ref<any>({});
const availableRelations = ref<any>({});
const showRelationSelector = ref(false);
const currentRelationField = ref("");
const relationSearchText = ref("");
const selectedRelationsToAdd = ref<string[]>([]);
const booksCache = ref<any[]>([]);
const isCreateMode = ref(false);
const createSelectedCollection = ref("");
const createFields = ref<any[]>([]);
const createData = ref<any>({});
const resultsFilter = ref("");

// Change messages / history
const showChangeHistory = ref(false);
const changeHistoryViewMode = ref<'friendly' | 'json'>('friendly');
const changeMessages = ref<any[]>([]);
const jsonErrors = ref<Record<number, string>>({});
const jsonEdited = ref<Record<number, boolean>>({});
const jsonEditedContent = ref<Record<number, string>>({});
const showChangeDetailModal = ref(false);
const selectedChangeDetail = ref<any>(null);
const showSaveConfirmDialog = ref(false);
const pendingSaveIndex = ref<number | null>(null);
const showReplayConfirmDialog = ref(false);
const replayProgress = ref<{ current: number; total: number; status: string } | null>(null);
const showReplayResultsDialog = ref(false);
const replayResults = ref<{ success: number; errors: Array<{ id: number; created: string; book: string; error: string; changeset: any[] }> }>({ success: 0, errors: [] });

// Super secret mode (secret mode + 4 dark mode toggles)
const superSecretMode = ref(false);
const lastToggleCount = ref(0);
const showDeleteDbConfirmDialog = ref(false);
const deleteDbProgress = ref<{ status: string } | null>(null);

// DB File Picker
const showDbPickerDialog = ref(false);
const availableDatabases = ref<Array<{ name: string; version: number | null }>>([]);
const sqliteDatabases = ref<string[]>([]);
const loadingDatabases = ref(false);

// Download All Changes
const showDownloadedChangesModal = ref(false);
const downloadedChanges = ref<any[]>([]);
const downloadedChangesLoading = ref(false);
const downloadedChangesError = ref("");

// Tech Handbook Browser
const showTechHandbook = ref(false);
const techHandbookData = ref<{ offerId: string; offerName: string; items: any[] }[]>([]);
const techHandbookLoading = ref(false);
const techHandbookError = ref("");

// Single Offer Tech Handbook (for /editor/offers/:id route)
const singleOfferTechHandbook = ref<{ offerId: string; offerName: string; items: any[] } | null>(null);
const singleOfferTechHandbookLoading = ref(false);
const singleOfferTechHandbookError = ref("");

// Computed property for filtered results
const filteredResults = computed(() => {
  if (!results.value?.values) return [];
  if (!resultsFilter.value.trim()) return results.value.values;

  const filterText = resultsFilter.value.toLowerCase();

  return results.value.values.filter((row: any) => {
    // Check all field values for the filter text
    for (const [key, value] of Object.entries(row)) {
      // Skip hidden fields
      if (shouldHideField(key)) continue;

      // Convert value to string for searching
      let searchValue = '';
      if (value === null || value === undefined) {
        searchValue = 'null';
      } else if (typeof value === 'object') {
        searchValue = JSON.stringify(value);
      } else {
        searchValue = String(value);
      }

      // Check if the value contains the filter text
      if (searchValue.toLowerCase().includes(filterText)) {
        return true;
      }

      // Also check formatted values for special fields
      if (isBookField(key)) {
        const formattedValue = formatBookValue(value);
        if (formattedValue.toLowerCase().includes(filterText)) {
          return true;
        }
      }
    }
    return false;
  });
});

// Allowed tables from changeset processor plus additional core tables
const allowedTables = [
  "menus",
  "offers",
  "menuTiers",
  "contentItems",
  "menuCopy",
  "problems",
  "problemTags",
  "checklists",
  "costsMaterial",
  "costsTime",
  "tierSets",
  "tiers",
  "formulas",
  "books",
];

// Fields that contain relation data (JSON arrays of IDs)
const relationFields = new Set([
  'menus', 'offers', 'menuTiers', 'contentItems', 'menuCopy',
  'problems', 'problemTags', 'checklists', 'costsMaterial',
  'costsTime', 'tierSets', 'tiers', 'formulas', 'books'
]);

const expandRelationField = async (dbConn: any, fieldName: string, value: any) => {
  if (!value || !relationFields.has(fieldName)) return value;

  try {
    const ids = typeof value === 'string' ? JSON.parse(value) : value;
    if (!Array.isArray(ids) || ids.length === 0) return value;

    const expandedData = [];
    for (const id of ids) {
      try {
        const relatedQuery = `SELECT * FROM ${fieldName} WHERE id = '${id}'`;
        const relatedResult = await dbConn.query(relatedQuery);
        if (relatedResult.values && relatedResult.values.length > 0) {
          expandedData.push(relatedResult.values[0]);
        }
      } catch (e) {
        // If we can't find the related record, just keep the ID
        expandedData.push({ id, _error: 'Record not found' });
      }
    }
    return expandedData;
  } catch (e) {
    // If parsing fails, return original value
    return value;
  }
};

const searchAllTablesForId = async (id: string) => {
  loading.value = true;
  error.value = "";
  results.value = null;
  lastQuery.value = `Searching all tables for ID: ${id}`;

  try {
    const { dbConn } = await getSQLite();
    let foundRecord = null;
    let foundTable = "";

    // Search through all allowed tables
    for (const table of allowedTables) {
      try {
        const query = `SELECT * FROM ${table} WHERE id = '${id}'`;
        const result = await dbConn.query(query);

        if (result.values && result.values.length > 0) {
          foundRecord = result.values[0];
          foundTable = table;
          break;
        }
      } catch (e) {
        // Table might not exist, continue searching
        continue;
      }
    }

    if (foundRecord) {
      // Store the original record data (including book field)
      originalData.value = { ...foundRecord };

      // Expand relation fields for the found record
      const expandedRow = { ...foundRecord };
      for (const [fieldName, value] of Object.entries(foundRecord)) {
        expandedRow[fieldName] = await expandRelationField(dbConn, fieldName, value);
      }

      results.value = {
        values: [expandedRow],
        foundInTable: foundTable
      };
      lastQuery.value = `Found in table: ${foundTable}`;
    } else {
      results.value = { values: [] };
      lastQuery.value = `ID "${id}" not found in any table`;
    }
  } catch (err: any) {
    error.value = err.message || "An error occurred while searching for the ID";
  } finally {
    loading.value = false;
  }
};

const executeQuery = async () => {
  if (!tableName.value.trim()) return;

  loading.value = true;
  error.value = "";
  results.value = null;
  resultsFilter.value = "";

  const query = `SELECT * FROM ${tableName.value.trim()}`;
  lastQuery.value = query;

  try {
    const { dbConn } = await getSQLite();
    const result = await dbConn.query(query);

    // Expand relation fields
    if (result.values && result.values.length > 0) {
      const expandedValues = [];
      for (const row of result.values) {
        const expandedRow = { ...row };
        for (const [fieldName, value] of Object.entries(row)) {
          expandedRow[fieldName] = await expandRelationField(dbConn, fieldName, value);
        }
        expandedValues.push(expandedRow);
      }
      result.values = expandedValues;
    }

    results.value = result;
  } catch (err: any) {
    error.value = err.message || "An error occurred while executing the query";
  } finally {
    loading.value = false;
  }
};

const formatValue = (value: any) => {
  if (value === null || value === undefined) return 'null';
  if (typeof value === 'object') return JSON.stringify(value, null, 2);
  return String(value);
};

const isIdField = (fieldName: string) => {
  return fieldName === 'id';
};

const isSystemField = (fieldName: string) => {
  return ['id', 'created', 'updated', 'refId'].includes(fieldName);
};

const shouldHideField = (fieldName: string) => {
  return fieldName === 'org';
};

const isCurrencyField = (fieldName: string) => {
  return fieldName === 'quantity';
};

const isBookField = (fieldName: string) => {
  return fieldName === 'book' || fieldName === 'books';
};

const getBookName = (bookId: string) => {
  const book = booksCache.value.find(b => b.id === bookId);
  return book ? book.name : bookId;
};

const formatBookValue = (value: any) => {
  // Handle single book ID
  if (typeof value === 'string') {
    return getBookName(value);
  }

  // Handle array of book IDs (e.g., for menus.books field)
  if (Array.isArray(value)) {
    return value.map(id => getBookName(id)).join(', ');
  }

  // Handle JSON string of book IDs
  if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
    try {
      const parsed = JSON.parse(value);
      if (Array.isArray(parsed)) {
        return parsed.map(id => getBookName(id)).join(', ');
      }
    } catch {
      // Not valid JSON, return as-is
    }
  }

  return value;
};

const loadAvailableRelations = async (relationFieldName: string) => {
  try {
    const { dbConn } = await getSQLite();
    const query = `SELECT DISTINCT id, name, refId FROM ${relationFieldName} ORDER BY name`;
    const result = await dbConn.query(query);

    if (result.values) {
      // Create a Map to ensure uniqueness by ID
      const uniqueRelations = new Map();

      result.values.forEach(item => {
        if (!uniqueRelations.has(item.id)) {
          const nameLabel = item.name || item.id;
          const refIdLabel = item.refId ? ` (${item.refId})` : '';

          uniqueRelations.set(item.id, {
            id: item.id,
            refId: item.refId || item.id, // Store refId for later use
            label: `${nameLabel}${refIdLabel}`,
            value: item.id
          });
        }
      });

      return Array.from(uniqueRelations.values());
    }
    return [];
  } catch (error) {
    console.warn(`Failed to load relations for ${relationFieldName}:`, error);
    return [];
  }
};


const navigateToId = (id: string) => {
  router.push(`/editor/${id}`);
};

const navigateToCreate = () => {
  router.push('/editor/create');
};

const lookupId = ref('');

const handleLookup = () => {
  if (lookupId.value.trim()) {
    router.push(`/editor/${lookupId.value.trim()}`);
  }
};

const browseTechHandbook = async () => {
  showTechHandbook.value = !showTechHandbook.value;

  if (!showTechHandbook.value) {
    return;
  }

  techHandbookLoading.value = true;
  techHandbookError.value = "";
  techHandbookData.value = [];

  try {
    const sql = await getSQLite();
    const query = `
      SELECT
        o.id AS offer_id,
        o.name AS offer_name,
        ci.*
      FROM offers o
      JOIN json_each(o.techhandbook) AS j
        ON 1=1
      JOIN contentItems ci
        ON ci.id = j.value
    `;

    const result = await sql.dbConn.query(query);

    if (result.values && result.values.length > 0) {
      // Group results by offer
      const groupedData: Map<string, { offerId: string; offerName: string; items: any[] }> = new Map();

      for (const row of result.values) {
        const offerId = row.offer_id;
        const offerName = row.offer_name;

        if (!groupedData.has(offerId)) {
          groupedData.set(offerId, { offerId, offerName, items: [] });
        }

        groupedData.get(offerId)!.items.push(row);
      }

      techHandbookData.value = Array.from(groupedData.values());
      console.log('[Editor] Tech Handbook loaded:', techHandbookData.value.length, 'offers');
    } else {
      techHandbookData.value = [];
    }
  } catch (e: any) {
    console.error('[Editor] Tech Handbook query failed:', e);
    techHandbookError.value = e.message || 'Failed to load tech handbook data';
  } finally {
    techHandbookLoading.value = false;
  }
};

const loadSingleOfferTechHandbook = async (offerId: string) => {
  singleOfferTechHandbookLoading.value = true;
  singleOfferTechHandbookError.value = "";
  singleOfferTechHandbook.value = null;

  try {
    const sql = await getSQLite();
    const query = `
      SELECT
        o.id AS offer_id,
        o.name AS offer_name,
        ci.*
      FROM offers o
      JOIN json_each(o.techhandbook) AS j
        ON 1=1
      JOIN contentItems ci
        ON ci.id = j.value
      WHERE o.id = '${offerId}'
    `;

    const result = await sql.dbConn.query(query);

    if (result.values && result.values.length > 0) {
      const firstRow = result.values[0];
      singleOfferTechHandbook.value = {
        offerId: firstRow.offer_id,
        offerName: firstRow.offer_name,
        items: result.values,
      };
      console.log('[Editor] Single Offer Tech Handbook loaded:', result.values.length, 'items');
    } else {
      singleOfferTechHandbook.value = null;
    }
  } catch (e: any) {
    console.error('[Editor] Single Offer Tech Handbook query failed:', e);
    singleOfferTechHandbookError.value = e.message || 'Failed to load tech handbook data';
  } finally {
    singleOfferTechHandbookLoading.value = false;
  }
};

const createNewBook = async () => {
  const { alertController, toastController } = await import('@ionic/vue');

  const alert = await alertController.create({
    header: 'Create New Book',
    inputs: [
      {
        name: 'name',
        type: 'text',
        placeholder: 'Book name (e.g., My Pricebook)',
      },
      {
        name: 'refId',
        type: 'text',
        placeholder: 'Reference ID (e.g., my-pricebook)',
      },
    ],
    buttons: [
      { text: 'Cancel', role: 'cancel' },
      {
        text: 'Create',
        handler: async (data) => {
          if (!data.name || !data.refId) {
            const toast = await toastController.create({
              message: 'Please provide both name and reference ID',
              duration: 3000,
              color: 'warning'
            });
            await toast.present();
            return false;
          }

          try {
            const { pb } = await getApi();
            const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;

            if (!activeOrg) {
              throw new Error('No active organization found');
            }

            // Create the book via PocketBase API
            const newBook = await pb.collection('books').create({
              name: data.name,
              refId: data.refId,
              org: activeOrg,
            });

            console.log('[Editor] Created new book:', newBook);

            // Refresh books in the local database
            const db = await getDb();
            if (db?.books?.refresh) {
              await db.books.refresh();
            }

            const toast = await toastController.create({
              message: `Book "${data.name}" created successfully`,
              duration: 3000,
              color: 'success'
            });
            await toast.present();

          } catch (e: any) {
            console.error('[Editor] Failed to create book:', e);
            const toast = await toastController.create({
              message: `Failed to create book: ${e.message}`,
              duration: 4000,
              color: 'danger'
            });
            await toast.present();
          }
        }
      }
    ]
  });

  await alert.present();
};

const goBackToBrowse = () => {
  router.push('/editor');
};

const removeRelation = (fieldName: string, relationId: string) => {
  if (editedRelations.value[fieldName]) {
    editedRelations.value[fieldName] = editedRelations.value[fieldName].filter((id: string) => id !== relationId);
  }
};

const onSingleRelationChange = (fieldName: string, event: any) => {
  // For single relations, the value is directly set via v-model
  // This handler is just for any additional processing if needed
  console.log(`Single relation ${fieldName} changed to:`, editedRelations.value[fieldName]);
};

const addRelation = (fieldName: string, relationIds: string[]) => {
  if (!editedRelations.value[fieldName]) {
    editedRelations.value[fieldName] = [];
  }
  // Add only new relations that aren't already present
  const newIds = relationIds.filter(id => !editedRelations.value[fieldName].includes(id));
  editedRelations.value[fieldName].push(...newIds);
};

const getRelationLabel = (fieldName: string, relationId: string) => {
  const relation = availableRelations.value[fieldName]?.find((r: any) => r.value === relationId);
  return relation?.label || relationId;
};

const getFilteredRelations = (fieldName: string) => {
  const available = availableRelations.value[fieldName] || [];
  const excluded = editedRelations.value[fieldName] || [];
  const filter = relationSearchText.value.toLowerCase() || '';

  return available
    .filter((opt: any) => !excluded.includes(opt.value))
    .filter((opt: any) => filter === '' || opt.label.toLowerCase().includes(filter));
};

const openRelationSelector = (fieldName: string) => {
  currentRelationField.value = fieldName;
  relationSearchText.value = "";
  selectedRelationsToAdd.value = [];
  showRelationSelector.value = true;
};

const closeRelationSelector = () => {
  showRelationSelector.value = false;
  currentRelationField.value = "";
  relationSearchText.value = "";
  selectedRelationsToAdd.value = [];
};

const toggleRelationSelection = (relationId: string) => {
  const index = selectedRelationsToAdd.value.indexOf(relationId);
  if (index > -1) {
    selectedRelationsToAdd.value.splice(index, 1);
  } else {
    selectedRelationsToAdd.value.push(relationId);
  }
};

const confirmRelationSelection = () => {
  if (selectedRelationsToAdd.value.length > 0) {
    addRelation(currentRelationField.value, selectedRelationsToAdd.value);
  }
  closeRelationSelector();
};

const resetToInitialState = () => {
  tableName.value = "";
  selectedTable.value = "";
  results.value = null;
  error.value = "";
  lastQuery.value = "";
  searchId.value = "";
  isIdMode.value = false;
  isEditMode.value = false;
  isCreateMode.value = false;
  editedData.value = {};
  originalData.value = {};
  editedRelations.value = {};
  availableRelations.value = {};
  createSelectedCollection.value = "";
  createFields.value = [];
  createData.value = {};
};

const startEditMode = async () => {
  if (results.value?.values?.[0]) {
    const record = results.value.values[0];
    originalData.value = { ...record };

    // Initialize editedData with current values (excluding system fields and relations)
    editedData.value = {};
    editedRelations.value = {};
    availableRelations.value = {};

    for (const [key, value] of Object.entries(record)) {
      if (!isSystemField(key) && !shouldHideField(key)) {
        if (Array.isArray(value) && typeof value[0] === 'object') {
          // This is a relation field - initialize for editing
          editedRelations.value[key] = value.map(item => item.id);
          // Load available options for this relation
          availableRelations.value[key] = await loadAvailableRelations(key);
        } else if (!Array.isArray(value)) {
          // Regular field or book field
          if (key === 'book') {
            // Book is a single relation - store the ID directly
            editedData.value[key] = value;
          } else if (isCurrencyField(key) && typeof value === 'number') {
            // Convert from base currency to user's preferred currency for editing
            try {
              const convertedValue = convertFromBaseCurrency(value);
              editedData.value[key] = convertedValue;
            } catch (error) {
              console.warn(`Failed to convert currency for ${key}:`, error);
              editedData.value[key] = value; // Fallback to original value
            }
          } else {
            editedData.value[key] = value;
          }
        }
      }
    }
    isEditMode.value = true;
  }
};

const cancelEdit = () => {
  isEditMode.value = false;
  editedData.value = {};
  originalData.value = {};
  editedRelations.value = {};
  availableRelations.value = {};
};

const confirmDelete = async () => {
  // Get the book ID from originalData (which has the raw record data)
  const bookId = originalData.value?.book || results.value?.values?.[0]?.book;

  // Get the refId from the original data
  const refId = originalData.value?.refId || results.value?.values?.[0]?.refId;

  // Get the book name for display
  let bookName = 'Unknown Book';
  if (bookId) {
    try {
      const { dbConn } = await getSQLite();
      const bookResult = await dbConn.query(
        `SELECT name FROM books WHERE id = '${bookId}'`
      );
      if (bookResult.values && bookResult.values.length > 0) {
        bookName = bookResult.values[0].name;
      }
    } catch (e) {
      console.warn('Could not fetch book name:', e);
    }
  }

  // Prepare delete changeset
  proposedChangeset.value = {
    changes: [{
      collection: results.value.foundInTable,
      operation: 'delete',
      data: {
        refId: refId
      }
    }],
    bookName: bookName,
    bookId: bookId,
    isDelete: true
  };

  showDeleteDialog.value = true;
};

const submitDelete = async () => {
  try {
    loading.value = true;

    const { pb } = await getApi();
    const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;

    if (!activeOrg) {
      throw new Error('No active organization found');
    }

    const bookId = proposedChangeset.value.bookId;
    if (!bookId) {
      throw new Error('Record does not have an associated book');
    }

    // Submit delete changeset
    const changesetRecord = await pb.collection('pricebookChanges').create({
      book: bookId,
      org: activeOrg,
      changeset: proposedChangeset.value.changes
    });

    console.log('Delete changeset submitted:', changesetRecord);

    showDeleteDialog.value = false;

    // Show success message
    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: 'Record deleted successfully. Syncing database...',
      duration: 2000,
      color: 'success'
    });
    await toast.present();

    // Sync the database
    const { syncDatabase } = await import('@/dataAccess/getDb');
    const syncResult = await syncDatabase();

    if (syncResult.success) {
      const syncToast = await toastController.create({
        message: 'Database synced successfully',
        duration: 2000,
        color: 'success'
      });
      await syncToast.present();
    }

    // Navigate back to browse since the record is deleted
    goBackToBrowse();

  } catch (error: any) {
    console.error('Error deleting record:', error);

    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: `Failed to delete record: ${error.message}`,
      duration: 4000,
      color: 'danger'
    });
    await toast.present();
  } finally {
    loading.value = false;
  }
};

const saveChanges = async () => {
  if (!originalData.value || !editedData.value) return;

  // Generate changeset in ChangesetProcessor format
  const data: any = {};
  let hasChanges = false;

  // Include refId for updates
  if (originalData.value.refId) {
    data.refId = originalData.value.refId;
  }

  // Check regular field changes and add all current values to data
  for (const [key, newValue] of Object.entries(editedData.value)) {
    const oldValue = originalData.value[key];

    let valueToCompare = newValue;
    let valueToSave = newValue;

    // Skip the book field here - it will be handled separately
    if (key === 'book') {
      // Book changes are tracked separately
      if (newValue !== oldValue) {
        hasChanges = true;
        data[key] = newValue; // Book ID is stored directly
      }
      continue;
    }

    // If this is a currency field, convert back to base currency for comparison and storage
    if (isCurrencyField(key) && typeof newValue === 'number') {
      try {
        valueToSave = convertToBaseCurrency(newValue);
        valueToCompare = valueToSave; // Compare converted values
      } catch (error) {
        console.warn(`Failed to convert currency for ${key}:`, error);
        // If conversion fails, use the original value (no change)
        valueToCompare = oldValue;
        valueToSave = oldValue;
      }
    }

    // Always include the field value in data
    data[key] = valueToSave;

    // Track if there are changes
    if (valueToCompare !== oldValue) {
      hasChanges = true;
    }
  }

  // Check relation changes and prepare refs structure
  const refs: any[] = [];
  for (const [key, newRelationIds] of Object.entries(editedRelations.value)) {
    const originalRelations = originalData.value[key];
    const originalIds = originalRelations?.map((item: any) => item.id) || [];

    // Compare arrays of IDs
    const idsChanged = JSON.stringify(originalIds.sort()) !== JSON.stringify((newRelationIds as string[]).sort());

    if (idsChanged) {
      hasChanges = true;
    }

    // Add to refs structure - include empty arrays to clear relations
    if (idsChanged || (newRelationIds && (newRelationIds as string[]).length > 0)) {
      const refIds = (newRelationIds as string[]).map((id: string) => {
        const relation = availableRelations.value[key]?.find((r: any) => r.value === id);
        return relation?.refId || id; // Use stored refId or fallback to id
      });
      refs.push({
        collection: key,
        refIds: refIds  // This will be an empty array if newRelationIds is empty
      });
    }
  }

  // Add refs to data if there are any
  if (refs.length > 0) {
    data.refs = refs;
  }

  if (hasChanges) {
    // Get the book name if we have a book ID
    let bookName = 'Unknown Book';
    if (originalData.value.book) {
      try {
        const { dbConn } = await getSQLite();
        const bookResult = await dbConn.query(
          `SELECT name FROM books WHERE id = '${originalData.value.book}'`
        );
        if (bookResult.values && bookResult.values.length > 0) {
          bookName = bookResult.values[0].name;
        }
      } catch (e) {
        console.warn('Could not fetch book name:', e);
      }
    }

    // Create changeset array with single update operation
    proposedChangeset.value = {
      changes: [{
        collection: results.value.foundInTable,
        operation: 'update',
        data: data
      }],
      bookName: bookName,
      bookId: originalData.value.book
    };
    showChangesetDialog.value = true;
  } else {
    // No changes to save
    cancelEdit();
  }
};

const submitChangeset = async () => {
  if (!proposedChangeset.value) return;

  loading.value = true;

  try {
    // Get the API instance
    const { pb } = await getApi();

    // Get the active organization
    const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;
    if (!activeOrg) {
      throw new Error('No active organization found');
    }

    // Get the book ID from the record itself - it's stored in the book column
    const bookId = originalData.value.book;

    if (!bookId) {
      throw new Error('Record does not have an associated book');
    }

    // Create the pricebookChanges record using the book's ID
    const changesetRecord = await pb.collection('pricebookChanges').create({
      book: bookId,
      org: activeOrg,
      changeset: proposedChangeset.value.changes
    });

    console.log('Changeset submitted:', changesetRecord);

    // Close the dialog
    showChangesetDialog.value = false;

    // Show success message
    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: 'Changes submitted successfully. Syncing database...',
      duration: 2000,
      color: 'success'
    });
    await toast.present();

    // Sync the database to pull down the changes
    const { syncDatabase } = await import('@/dataAccess/getDb');
    const syncResult = await syncDatabase();

    if (syncResult.success) {
      const syncToast = await toastController.create({
        message: 'Database synced successfully',
        duration: 2000,
        color: 'success'
      });
      await syncToast.present();

      // Refresh the current data by re-fetching
      if (isIdMode.value) {
        await searchAllTablesForId(searchId.value);
      }
    } else {
      const syncToast = await toastController.create({
        message: syncResult.connected ? 'Sync completed with warnings' : 'Offline - changes saved to server',
        duration: 3000,
        color: 'warning'
      });
      await syncToast.present();
    }

    // Exit edit mode
    cancelEdit();

  } catch (error: any) {
    console.error('Error submitting changeset:', error);

    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: `Failed to submit changes: ${error.message}`,
      duration: 4000,
      color: 'danger'
    });
    await toast.present();
  } finally {
    loading.value = false;
  }
};

// Change messages functions
const loadChangeMessages = async () => {
  try {
    const sql = await getSQLite();
    const result = await sql.dbConn.query(
      `SELECT id, book, changeset, status, error_message, created
       FROM pricebookChanges
       ORDER BY created DESC
       LIMIT 100`
    );

    if (result.values && result.values.length > 0) {
      // Parse the changeset JSON for each row
      changeMessages.value = result.values.map((row: any) => ({
        ...row,
        changeset: row.changeset ? JSON.parse(row.changeset) : [],
      }));
      console.log('[Editor] Loaded', changeMessages.value.length, 'pricebook changes');
    } else {
      changeMessages.value = [];
    }
  } catch (e: any) {
    console.error('[Editor] Failed to load pricebook changes:', e);
    changeMessages.value = [];
  }
};

const toggleChangeHistory = async () => {
  showChangeHistory.value = !showChangeHistory.value;
  if (showChangeHistory.value) {
    await loadChangeMessages();
    // Reset edit state
    jsonEdited.value = {};
    jsonEditedContent.value = {};
    jsonErrors.value = {};
  }
};

const formatDate = (dateStr: string) => {
  try {
    const date = new Date(dateStr);
    return date.toLocaleString();
  } catch {
    return dateStr;
  }
};

const getPreviewData = (data: any) => {
  if (!data) return {};
  const preview: Record<string, any> = {};
  const keys = Object.keys(data).filter(k => k !== 'refs').slice(0, 4);
  for (const key of keys) {
    preview[key] = data[key];
  }
  return preview;
};

const truncateValue = (value: any) => {
  const str = typeof value === 'object' ? JSON.stringify(value) : String(value);
  return str.length > 50 ? str.substring(0, 50) + '...' : str;
};

const expandChange = (msg: any, changeIndex: number) => {
  selectedChangeDetail.value = msg.changeset[changeIndex];
  showChangeDetailModal.value = true;
};

const formatJsonForEdit = (msg: any) => {
  return JSON.stringify({
    id: msg.id,
    book: msg.book,
    status: msg.status,
    created: msg.created,
    changeset: msg.changeset
  }, null, 2);
};

const updateChangesetJson = (msgIndex: number, event: Event) => {
  const target = event.target as HTMLTextAreaElement;
  const originalJson = formatJsonForEdit(changeMessages.value[msgIndex]);

  // Track if content has been edited
  jsonEdited.value[msgIndex] = target.value !== originalJson;
  jsonEditedContent.value[msgIndex] = target.value;

  try {
    JSON.parse(target.value);
    jsonErrors.value[msgIndex] = '';
  } catch (e: any) {
    jsonErrors.value[msgIndex] = `Invalid JSON: ${e.message}`;
  }
};

const saveChangeset = (msgIndex: number) => {
  pendingSaveIndex.value = msgIndex;
  showSaveConfirmDialog.value = true;
};

const confirmSaveChangeset = async () => {
  const msgIndex = pendingSaveIndex.value;
  if (msgIndex === null) return;

  const editedJson = jsonEditedContent.value[msgIndex];
  if (!editedJson) return;

  try {
    const parsed = JSON.parse(editedJson);
    const msg = changeMessages.value[msgIndex];

    // Update the database record
    const db = await getDb();
    if (db) {
      await db.changeMessages.update(msg.id, parsed.changeset);
    }

    // Update the local changeMessages array
    changeMessages.value[msgIndex] = {
      ...msg,
      changeset: parsed.changeset
    };

    // Clear the edited state
    jsonEdited.value[msgIndex] = false;
    delete jsonEditedContent.value[msgIndex];

    showSaveConfirmDialog.value = false;
    pendingSaveIndex.value = null;

    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: 'Change message updated in database',
      duration: 2000,
      color: 'success'
    });
    await toast.present();
  } catch (e: any) {
    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: `Failed to save: ${e.message}`,
      duration: 3000,
      color: 'danger'
    });
    await toast.present();
  }
};

const cancelSaveChangeset = () => {
  showSaveConfirmDialog.value = false;
  pendingSaveIndex.value = null;
};

const confirmReplay = async () => {
  showReplayConfirmDialog.value = false;
  replayProgress.value = { current: 0, total: 0, status: 'Preparing...' };
  replayResults.value = { success: 0, errors: [] };

  try {
    const { dbConn, saveDb } = await getSQLite();
    const { pb } = await getApi();
    const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;

    if (!activeOrg) {
      throw new Error('No active organization found');
    }

    // Tables to clear (all pricebook data tables)
    const tablesToClear = [
      'menus',
      'offers',
      'menuTiers',
      'contentItems',
      'menuCopy',
      'warrantyCopy',
      'problems',
      'problemTags',
      'checklists',
      'costsMaterial',
      'costsTime',
      'tierSets',
      'tiers',
      'formulas'
    ];

    replayProgress.value = { current: 0, total: tablesToClear.length + changeMessages.value.length, status: 'Clearing tables...' };

    // Clear all pricebook data tables
    for (const table of tablesToClear) {
      try {
        await dbConn.execute(`DELETE FROM ${table}`);
        replayProgress.value.current++;
        replayProgress.value.status = `Cleared ${table}`;
      } catch (e) {
        console.warn(`Could not clear table ${table}:`, e);
      }
    }

    await saveDb();

    // Sort changeMessages by created date (oldest first)
    const sortedMessages = [...changeMessages.value].sort((a, b) =>
      new Date(a.created).getTime() - new Date(b.created).getTime()
    );

    replayProgress.value.status = 'Replaying changes...';

    // Process each changeset
    const processor = new ChangesetProcessor(dbConn);
    const db = await getDb();
    let successCount = 0;
    const errors: Array<{ id: number; created: string; book: string; error: string; changeset: any[] }> = [];

    for (let i = 0; i < sortedMessages.length; i++) {
      const msg = sortedMessages[i];
      replayProgress.value.current = tablesToClear.length + i + 1;
      replayProgress.value.status = `Processing change ${i + 1} of ${sortedMessages.length}...`;

      try {
        await processor.processChangeset(
          msg.changeset,
          activeOrg,
          false, // Don't use transaction per change, we'll save at the end
          msg.book
        );

        // Update status in changeMessages table
        if (db) {
          await dbConn.run(
            `UPDATE changeMessages SET status = 'success', error_message = NULL WHERE id = ?`,
            [msg.id]
          );
        }
        successCount++;
      } catch (e: any) {
        const errorMessage = e.message || String(e);
        console.error(`Error replaying change ${msg.id}:`, e);

        // Track error for results log
        errors.push({
          id: msg.id,
          created: msg.created,
          book: msg.book,
          error: errorMessage,
          changeset: msg.changeset
        });

        // Update status in changeMessages table
        if (db) {
          await dbConn.run(
            `UPDATE changeMessages SET status = 'error', error_message = ? WHERE id = ?`,
            [errorMessage, msg.id]
          );
        }
      }
    }

    await saveDb();

    replayProgress.value = null;

    // Store results
    replayResults.value = { success: successCount, errors };

    // Reload change messages to show updated statuses
    await loadChangeMessages();

    // Show results dialog if there were any errors, otherwise just show toast
    if (errors.length > 0) {
      showReplayResultsDialog.value = true;
    } else {
      const { toastController } = await import('@ionic/vue');
      const toast = await toastController.create({
        message: `Replay complete: ${successCount} changes applied successfully`,
        duration: 4000,
        color: 'success'
      });
      await toast.present();
    }

  } catch (e: any) {
    replayProgress.value = null;
    console.error('Replay failed:', e);

    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: `Replay failed: ${e.message}`,
      duration: 4000,
      color: 'danger'
    });
    await toast.present();
  }
};

const cancelReplay = () => {
  showReplayConfirmDialog.value = false;
};

const confirmDeleteDatabase = async () => {
  showDeleteDbConfirmDialog.value = false;
  deleteDbProgress.value = { status: 'Preparing to restore from default...' };

  try {
    // Step 1: Copy default.sql to pricebookPlatformSQLite.db in jeepSqliteStore
    deleteDbProgress.value = { status: 'Restoring from default.sql...' };

    await new Promise<void>((resolve, reject) => {
      const request = indexedDB.open('jeepSqliteStore');

      request.onerror = () => reject(new Error('Could not open jeepSqliteStore'));

      request.onsuccess = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;

        if (!db.objectStoreNames.contains('databases')) {
          db.close();
          reject(new Error('databases object store not found'));
          return;
        }

        const transaction = db.transaction(['databases'], 'readwrite');
        const store = transaction.objectStore('databases');

        // Get the default database
        const getRequest = store.get('default.sql');

        getRequest.onsuccess = () => {
          if (!getRequest.result) {
            db.close();
            reject(new Error('default.sql not found. Please save a default database first using "Save as Default".'));
            return;
          }

          // Delete the current database
          const deleteRequest = store.delete('pricebookPlatformSQLite.db');

          deleteRequest.onsuccess = () => {
            // Copy default.sql to pricebookPlatformSQLite.db
            const putRequest = store.put(getRequest.result, 'pricebookPlatformSQLite.db');

            putRequest.onsuccess = () => {
              db.close();
              resolve();
            };

            putRequest.onerror = () => {
              db.close();
              reject(new Error('Failed to restore database'));
            };
          };

          deleteRequest.onerror = () => {
            db.close();
            reject(new Error('Failed to delete current database'));
          };
        };

        getRequest.onerror = () => {
          db.close();
          reject(new Error('Failed to read default.sql'));
        };
      };
    });

    deleteDbProgress.value = { status: 'Database restored. Reloading...' };
    await new Promise(resolve => setTimeout(resolve, 500));

    // Reload the page to get fresh database connections
    window.location.reload();

  } catch (e: any) {
    deleteDbProgress.value = null;
    console.error('Delete database failed:', e);

    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: `Failed to delete database: ${e.message}`,
      duration: 4000,
      color: 'danger'
    });
    await toast.present();
  }
};

// DB File Picker functions
const openDbPicker = async () => {
  showDbPickerDialog.value = true;
  await refreshDatabaseList();
};

const refreshDatabaseList = async () => {
  loadingDatabases.value = true;
  availableDatabases.value = [];
  sqliteDatabases.value = [];

  try {
    if ('indexedDB' in window && indexedDB.databases) {
      const databases = await indexedDB.databases();
      availableDatabases.value = databases.map(db => ({
        name: db.name || 'Unknown',
        version: db.version || null
      }));

      // Try to get SQLite databases from jeepSqliteStore
      await loadSqliteDatabasesFromStore();
    } else {
      // Fallback: indexedDB.databases() not supported in all browsers
      console.warn('indexedDB.databases() not supported');
    }
  } catch (e) {
    console.error('Failed to list databases:', e);
  } finally {
    loadingDatabases.value = false;
  }
};

const loadSqliteDatabasesFromStore = async () => {
  return new Promise<void>((resolve) => {
    const request = indexedDB.open('jeepSqliteStore');

    request.onerror = () => {
      console.warn('Could not open jeepSqliteStore');
      resolve();
    };

    request.onsuccess = (event) => {
      const db = (event.target as IDBOpenDBRequest).result;

      // Check if the 'databases' object store exists
      if (!db.objectStoreNames.contains('databases')) {
        db.close();
        resolve();
        return;
      }

      const transaction = db.transaction(['databases'], 'readonly');
      const store = transaction.objectStore('databases');
      const getAllRequest = store.getAllKeys();

      getAllRequest.onsuccess = () => {
        sqliteDatabases.value = (getAllRequest.result as string[]).filter(
          key => typeof key === 'string'
        );
        db.close();
        resolve();
      };

      getAllRequest.onerror = () => {
        db.close();
        resolve();
      };
    };

    request.onupgradeneeded = () => {
      // Database doesn't exist or is being created, just close it
      request.transaction?.abort();
      resolve();
    };
  });
};

const saveAsDefaultDb = async () => {
  const { alertController, toastController } = await import('@ionic/vue');

  const alert = await alertController.create({
    header: 'Save as Default',
    message: 'This will save the current database as "default.sql" in jeepSqliteStore. This can be used as a backup or starting point. Continue?',
    buttons: [
      {
        text: 'Cancel',
        role: 'cancel'
      },
      {
        text: 'Save',
        handler: async () => {
          try {
            // First, ensure the current database is saved to store
            const { saveDb } = await getSQLite();
            await saveDb();

            // Now copy from pricebookPlatform to default.sql in jeepSqliteStore
            await new Promise<void>((resolve, reject) => {
              const request = indexedDB.open('jeepSqliteStore');

              request.onerror = () => reject(new Error('Could not open jeepSqliteStore'));

              request.onsuccess = (event) => {
                const db = (event.target as IDBOpenDBRequest).result;

                if (!db.objectStoreNames.contains('databases')) {
                  db.close();
                  reject(new Error('databases object store not found'));
                  return;
                }

                const transaction = db.transaction(['databases'], 'readwrite');
                const store = transaction.objectStore('databases');

                // Get the current database data (jeep-sqlite adds "SQLite.db" suffix)
                const getRequest = store.get('pricebookPlatformSQLite.db');

                getRequest.onsuccess = () => {
                  if (!getRequest.result) {
                    db.close();
                    reject(new Error('pricebookPlatformSQLite.db not found'));
                    return;
                  }

                  // Save as default.sql
                  const putRequest = store.put(getRequest.result, 'default.sql');

                  putRequest.onsuccess = () => {
                    db.close();
                    resolve();
                  };

                  putRequest.onerror = () => {
                    db.close();
                    reject(new Error('Failed to save default.sql'));
                  };
                };

                getRequest.onerror = () => {
                  db.close();
                  reject(new Error('Failed to read pricebookPlatform'));
                };
              };
            });

            const toast = await toastController.create({
              message: 'Database saved as "default.sql"',
              duration: 2000,
              color: 'success'
            });
            await toast.present();

            // Refresh the SQLite databases list if picker is open
            await loadSqliteDatabasesFromStore();
          } catch (e: any) {
            const toast = await toastController.create({
              message: `Failed to save: ${e.message}`,
              duration: 3000,
              color: 'danger'
            });
            await toast.present();
          }
        }
      }
    ]
  });

  await alert.present();
};

const uploadDefaultDbToServer = async () => {
  const { toastController, alertController } = await import('@ionic/vue');

  try {
    // Read default.sql from jeepSqliteStore
    const binaryData = await new Promise<ArrayBuffer>((resolve, reject) => {
      const request = indexedDB.open('jeepSqliteStore');

      request.onerror = () => reject(new Error('Could not open jeepSqliteStore'));

      request.onsuccess = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;

        if (!db.objectStoreNames.contains('databases')) {
          db.close();
          reject(new Error('databases object store not found'));
          return;
        }

        const transaction = db.transaction(['databases'], 'readonly');
        const store = transaction.objectStore('databases');
        const getRequest = store.get('default.sql');

        getRequest.onsuccess = () => {
          if (!getRequest.result) {
            db.close();
            reject(new Error('default.sql not found. Please save a default database first using "Save as Default".'));
            return;
          }
          db.close();
          resolve(getRequest.result);
        };

        getRequest.onerror = () => {
          db.close();
          reject(new Error('Failed to read default.sql'));
        };
      };
    });

    // Confirm upload
    const alert = await alertController.create({
      header: 'Upload Default Database',
      message: `This will upload the default database (${(binaryData.byteLength / 1024 / 1024).toFixed(2)} MB) to the server. This will replace any existing default database. Continue?`,
      buttons: [
        { text: 'Cancel', role: 'cancel' },
        {
          text: 'Upload',
          handler: async () => {
            try {
              const { pb } = await getApi();
              const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;

              if (!activeOrg) {
                throw new Error('No active organization found');
              }

              // Create a File from the binary data
              const file = new File([binaryData], 'default.db', { type: 'application/octet-stream' });

              // Check if a default database record already exists for this org
              const existing = await pb.collection('defaultData').getList(1, 1, {
                filter: `org = '${activeOrg}'`
              });

              if (existing.items.length > 0) {
                // Update existing record
                await pb.collection('defaultData').update(existing.items[0].id, {
                  database: file
                });
              } else {
                // Create new record
                await pb.collection('defaultData').create({
                  org: activeOrg,
                  database: file
                });
              }

              const toast = await toastController.create({
                message: 'Default database uploaded to server successfully',
                duration: 3000,
                color: 'success'
              });
              await toast.present();

            } catch (e: any) {
              const toast = await toastController.create({
                message: `Upload failed: ${e.message}`,
                duration: 4000,
                color: 'danger'
              });
              await toast.present();
            }
          }
        }
      ]
    });
    await alert.present();

  } catch (e: any) {
    const toast = await toastController.create({
      message: `Failed to read database: ${e.message}`,
      duration: 3000,
      color: 'danger'
    });
    await toast.present();
  }
};

const downloadDefaultDbFromServer = async () => {
  const { toastController, alertController } = await import('@ionic/vue');

  try {
    const { pb } = await getApi();
    const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;

    if (!activeOrg) {
      throw new Error('No active organization found');
    }

    // Fetch default database record for this org
    const records = await pb.collection('defaultData').getList(1, 1, {
      filter: `org = '${activeOrg}'`
    });

    if (records.items.length === 0) {
      throw new Error('No default database found for this organization');
    }

    const record = records.items[0];
    if (!record.database) {
      throw new Error('Default database record has no file attached');
    }

    // Get timestamp for display
    const timestamp = record.updated || record.created;

    // Confirm download
    const alert = await alertController.create({
      header: 'Download Default Database',
      message: `This will download the default database from the server (last updated: ${new Date(timestamp).toLocaleString()}) and replace your local database. You will need to reload the page. Continue?`,
      buttons: [
        { text: 'Cancel', role: 'cancel' },
        {
          text: 'Download',
          handler: async () => {
            try {
              // Download the database file
              const fileUrl = pb.files.getURL(record, record.database);
              const response = await fetch(fileUrl);
              if (!response.ok) {
                throw new Error('Failed to download default database');
              }

              const arrayBuffer = await response.arrayBuffer();
              console.log('[Editor] Downloaded default.db from server, size:', arrayBuffer.byteLength);

              // Store the database in jeepSqliteStore (same pattern as confirmDeleteDatabase)
              await new Promise<void>((resolve, reject) => {
                const request = indexedDB.open('jeepSqliteStore');

                request.onerror = () => reject(new Error('Could not open jeepSqliteStore'));

                request.onsuccess = (event) => {
                  const db = (event.target as IDBOpenDBRequest).result;

                  if (!db.objectStoreNames.contains('databases')) {
                    db.close();
                    reject(new Error('databases object store not found'));
                    return;
                  }

                  const transaction = db.transaction(['databases'], 'readwrite');
                  const store = transaction.objectStore('databases');

                  // Delete the current database first
                  const deleteRequest = store.delete('pricebookPlatformSQLite.db');

                  deleteRequest.onsuccess = () => {
                    // Then save the new database
                    const putRequest = store.put(arrayBuffer, 'pricebookPlatformSQLite.db');

                    putRequest.onsuccess = () => {
                      db.close();
                      console.log('[Editor] Default database saved to IndexedDB');
                      resolve();
                    };

                    putRequest.onerror = () => {
                      db.close();
                      reject(new Error('Failed to save database'));
                    };
                  };

                  deleteRequest.onerror = () => {
                    db.close();
                    reject(new Error('Failed to delete current database'));
                  };
                };
              });

              // Store timestamp in localStorage for sync
              localStorage.setItem('defaultDbTimestamp', timestamp);

              const toast = await toastController.create({
                message: 'Default database downloaded. Reloading...',
                duration: 2000,
                color: 'success'
              });
              await toast.present();

              // Reload after a short delay
              setTimeout(() => {
                window.location.reload();
              }, 1500);

            } catch (e: any) {
              const toast = await toastController.create({
                message: `Download failed: ${e.message}`,
                duration: 4000,
                color: 'danger'
              });
              await toast.present();
            }
          }
        }
      ]
    });
    await alert.present();

  } catch (e: any) {
    const toast = await toastController.create({
      message: `Failed: ${e.message}`,
      duration: 3000,
      color: 'danger'
    });
    await toast.present();
  }
};

const deleteSpecificDb = async (dbName: string) => {
  const { alertController, toastController } = await import('@ionic/vue');

  const alert = await alertController.create({
    header: 'Delete Database',
    message: `Are you sure you want to delete "${dbName}"? This cannot be undone.`,
    buttons: [
      {
        text: 'Cancel',
        role: 'cancel'
      },
      {
        text: 'Delete',
        role: 'destructive',
        handler: async () => {
          try {
            await new Promise<void>((resolve, reject) => {
              const deleteRequest = indexedDB.deleteDatabase(dbName);
              deleteRequest.onsuccess = () => resolve();
              deleteRequest.onerror = () => reject(deleteRequest.error);
              deleteRequest.onblocked = () => {
                console.warn(`Delete blocked for ${dbName}`);
                resolve();
              };
            });

            const toast = await toastController.create({
              message: `Database "${dbName}" deleted`,
              duration: 2000,
              color: 'success'
            });
            await toast.present();

            // Refresh the list
            await refreshDatabaseList();
          } catch (e: any) {
            const toast = await toastController.create({
              message: `Failed to delete: ${e.message}`,
              duration: 3000,
              color: 'danger'
            });
            await toast.present();
          }
        }
      }
    ]
  });

  await alert.present();
};

const copyReplayLog = async () => {
  const log = {
    timestamp: new Date().toISOString(),
    summary: {
      success: replayResults.value.success,
      failed: replayResults.value.errors.length
    },
    errors: replayResults.value.errors.map(err => ({
      id: err.id,
      created: err.created,
      book: err.book,
      bookName: getBookName(err.book),
      error: err.error,
      changeset: err.changeset
    }))
  };

  await navigator.clipboard.writeText(JSON.stringify(log, null, 2));

  const { toastController } = await import('@ionic/vue');
  const toast = await toastController.create({
    message: 'Replay log copied to clipboard',
    duration: 1500,
    color: 'success'
  });
  await toast.present();
};

const copyMessageJson = async (msg: any) => {
  const json = formatJsonForEdit(msg);
  await navigator.clipboard.writeText(json);
  const { toastController } = await import('@ionic/vue');
  const toast = await toastController.create({
    message: 'JSON copied to clipboard',
    duration: 1500,
    color: 'success'
  });
  await toast.present();
};

const copyAllJson = async () => {
  const allData = changeMessages.value.map(msg => ({
    id: msg.id,
    book: msg.book,
    status: msg.status,
    created: msg.created,
    changeset: msg.changeset
  }));
  await navigator.clipboard.writeText(JSON.stringify(allData, null, 2));
  const { toastController } = await import('@ionic/vue');
  const toast = await toastController.create({
    message: 'All JSON copied to clipboard',
    duration: 1500,
    color: 'success'
  });
  await toast.present();
};

const deleteChangesetLocally = async (id: string) => {
  const { alertController, toastController } = await import('@ionic/vue');

  const alert = await alertController.create({
    header: 'Delete Changeset',
    message: 'Are you sure you want to delete this changeset from the local database? This cannot be undone.',
    buttons: [
      { text: 'Cancel', role: 'cancel' },
      {
        text: 'Delete',
        role: 'destructive',
        handler: async () => {
          try {
            const sql = await getSQLite();

            // Delete from changeMessages table
            await sql.dbConn.execute(`DELETE FROM changeMessages WHERE id = '${id}'`);
            await sql.saveDb();

            // Remove from the local array
            changeMessages.value = changeMessages.value.filter(msg => msg.id !== id);

            const toast = await toastController.create({
              message: 'Changeset deleted locally',
              duration: 2000,
              color: 'success'
            });
            await toast.present();

          } catch (e: any) {
            console.error('[Editor] Failed to delete changeset:', e);
            const toast = await toastController.create({
              message: `Failed to delete: ${e.message}`,
              duration: 3000,
              color: 'danger'
            });
            await toast.present();
          }
        }
      }
    ]
  });

  await alert.present();
};

const downloadAndViewAllChanges = async () => {
  showDownloadedChangesModal.value = true;
  downloadedChangesLoading.value = true;
  downloadedChangesError.value = "";
  downloadedChanges.value = [];

  try {
    const { pb } = await getApi();

    // Get all pricebook changes from the API
    const allChanges = await pb.collection('pricebookChanges').getFullList({
      sort: 'created'
    });

    downloadedChanges.value = allChanges;
    console.log(`[Editor] Downloaded ${allChanges.length} changesets from API`);
  } catch (e: any) {
    console.error('[Editor] Failed to download changes:', e);
    downloadedChangesError.value = `Failed to download changes: ${e.message}`;
  } finally {
    downloadedChangesLoading.value = false;
  }
};

const copyAllDownloadedChanges = async () => {
  const { toastController } = await import('@ionic/vue');

  try {
    const jsonStr = JSON.stringify(downloadedChanges.value, null, 2);
    await navigator.clipboard.writeText(jsonStr);

    const toast = await toastController.create({
      message: 'Copied all changes to clipboard',
      duration: 2000,
      color: 'success'
    });
    await toast.present();
  } catch (e: any) {
    const toast = await toastController.create({
      message: `Failed to copy: ${e.message}`,
      duration: 3000,
      color: 'danger'
    });
    await toast.present();
  }
};

const showCombineChangesDialog = async () => {
  const { alertController, toastController } = await import('@ionic/vue');

  if (changeMessages.value.length === 0) {
    const toast = await toastController.create({
      message: 'No changes to combine',
      duration: 2000,
      color: 'warning'
    });
    await toast.present();
    return;
  }

  try {
    // Get available books from the API
    const { pb } = await getApi();
    const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;

    if (!activeOrg) {
      throw new Error('No active organization found');
    }

    const books = await pb.collection('books').getFullList({
      filter: `org = '${activeOrg}'`,
      sort: 'name'
    });

    if (books.length === 0) {
      const toast = await toastController.create({
        message: 'No books found. Create a book first.',
        duration: 3000,
        color: 'warning'
      });
      await toast.present();
      return;
    }

    // Build radio options for book selection
    const bookInputs = books.map((book, index) => ({
      name: 'book',
      type: 'radio' as const,
      label: `${book.name} (${book.refId})`,
      value: book.id,
      checked: index === 0
    }));

    const alert = await alertController.create({
      header: 'Combine Changes',
      message: `This will combine ${changeMessages.value.length} change(s) into a single changeset. Select the target book:`,
      inputs: bookInputs,
      buttons: [
        { text: 'Cancel', role: 'cancel' },
        {
          text: 'Combine & Create',
          handler: async (selectedBookId) => {
            if (!selectedBookId) {
              const toast = await toastController.create({
                message: 'Please select a book',
                duration: 2000,
                color: 'warning'
              });
              await toast.present();
              return false;
            }

            try {
              const BATCH_SIZE = 650;

              // Sort messages by created date (oldest first) and combine all changesets
              const sortedMessages = [...changeMessages.value].sort((a, b) =>
                new Date(a.created).getTime() - new Date(b.created).getTime()
              );

              const allOperations: any[] = [];
              for (const msg of sortedMessages) {
                if (msg.changeset && Array.isArray(msg.changeset)) {
                  allOperations.push(...msg.changeset);
                }
              }

              console.log('[Editor] Total operations to combine:', allOperations.length);

              // Split into batches of BATCH_SIZE
              const batches: any[][] = [];
              for (let i = 0; i < allOperations.length; i += BATCH_SIZE) {
                batches.push(allOperations.slice(i, i + BATCH_SIZE));
              }

              console.log('[Editor] Creating', batches.length, 'batch(es) of up to', BATCH_SIZE, 'operations each');

              // Create a pricebookChanges record for each batch
              let createdCount = 0;
              const timestamp = Date.now();
              for (let i = 0; i < batches.length; i++) {
                const batch = batches[i];
                const refId = `combined-${timestamp}-${i + 1}`;
                console.log(`[Editor] Creating batch ${i + 1}/${batches.length} with ${batch.length} operations (refId: ${refId})`);

                await pb.collection('pricebookChanges').create({
                  book: selectedBookId,
                  org: activeOrg,
                  refId: refId,
                  changeset: batch
                });
                createdCount++;
              }

              console.log('[Editor] Created', createdCount, 'changeset record(s)');

              const selectedBook = books.find(b => b.id === selectedBookId);
              const toast = await toastController.create({
                message: `Created ${createdCount} changeset(s) with ${allOperations.length} total operations for "${selectedBook?.name || selectedBookId}"`,
                duration: 4000,
                color: 'success'
              });
              await toast.present();

            } catch (e: any) {
              console.error('[Editor] Failed to create combined change:', e);
              // Log detailed error info from PocketBase
              if (e.data) {
                console.error('[Editor] PocketBase error data:', e.data);
              }
              if (e.response) {
                console.error('[Editor] PocketBase response:', e.response);
              }
              const toast = await toastController.create({
                message: `Failed to combine changes: ${e.message}`,
                duration: 4000,
                color: 'danger'
              });
              await toast.present();
            }
          }
        }
      ]
    });

    await alert.present();

  } catch (e: any) {
    console.error('[Editor] Failed to load books:', e);
    const toast = await toastController.create({
      message: `Failed to load books: ${e.message}`,
      duration: 3000,
      color: 'danger'
    });
    await toast.present();
  }
};

const pushChangesToServer = async () => {
  const { alertController, toastController } = await import('@ionic/vue');

  try {
    // Get the last sync time from the database
    const sql = await getSQLite();
    const lastSyncResult = await sql.dbConn.query(
      "SELECT value FROM appStatus WHERE key = 'lastSync'"
    );
    const lastSyncTime = lastSyncResult.values?.[0]?.value || '1970-01-01T00:00:00.000Z';
    const lastSyncDate = new Date(lastSyncTime);

    console.log('[Editor] Last sync time:', lastSyncTime);

    // Filter changes that are newer than the last sync
    const newChanges = changeMessages.value.filter(msg => {
      const msgDate = new Date(msg.created);
      return msgDate > lastSyncDate;
    });

    if (newChanges.length === 0) {
      const toast = await toastController.create({
        message: `No new changes since last sync (${lastSyncDate.toLocaleString()})`,
        duration: 3000,
        color: 'warning'
      });
      await toast.present();
      return;
    }

    // Get available books from the API
    const { pb } = await getApi();
    const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;

    if (!activeOrg) {
      throw new Error('No active organization found');
    }

    const books = await pb.collection('books').getFullList({
      filter: `org = '${activeOrg}'`,
      sort: 'name'
    });

    if (books.length === 0) {
      const toast = await toastController.create({
        message: 'No books found. Create a book first.',
        duration: 3000,
        color: 'warning'
      });
      await toast.present();
      return;
    }

    // Build radio options for book selection
    const bookInputs = books.map((book, index) => ({
      name: 'book',
      type: 'radio' as const,
      label: `${book.name} (${book.refId})`,
      value: book.id,
      checked: index === 0
    }));

    const alert = await alertController.create({
      header: 'Push Changes to Server',
      message: `Found ${newChanges.length} change(s) newer than last sync (${lastSyncDate.toLocaleString()}). Select the target book:`,
      inputs: bookInputs,
      buttons: [
        { text: 'Cancel', role: 'cancel' },
        {
          text: 'Push New Changes',
          handler: async (selectedBookId) => {
            if (!selectedBookId) {
              const toast = await toastController.create({
                message: 'Please select a book',
                duration: 2000,
                color: 'warning'
              });
              await toast.present();
              return false;
            }

            try {
              // Sort messages by created date (oldest first)
              const sortedMessages = [...newChanges].sort((a, b) =>
                new Date(a.created).getTime() - new Date(b.created).getTime()
              );

              console.log('[Editor] Pushing', sortedMessages.length, 'new change(s) to server');

              let pushedCount = 0;
              const timestamp = Date.now();

              for (let i = 0; i < sortedMessages.length; i++) {
                const msg = sortedMessages[i];
                const refId = `push-${timestamp}-${i + 1}`;

                if (!msg.changeset || !Array.isArray(msg.changeset) || msg.changeset.length === 0) {
                  console.log(`[Editor] Skipping empty changeset at index ${i}`);
                  continue;
                }

                console.log(`[Editor] Pushing change ${i + 1}/${sortedMessages.length} with ${msg.changeset.length} operations`);

                await pb.collection('pricebookChanges').create({
                  book: selectedBookId,
                  org: activeOrg,
                  refId: refId,
                  changeset: msg.changeset
                });
                pushedCount++;
              }

              console.log('[Editor] Pushed', pushedCount, 'changeset(s) to server');

              const selectedBook = books.find(b => b.id === selectedBookId);
              const toast = await toastController.create({
                message: `Pushed ${pushedCount} changeset(s) to "${selectedBook?.name || selectedBookId}"`,
                duration: 4000,
                color: 'success'
              });
              await toast.present();

            } catch (e: any) {
              console.error('[Editor] Failed to push changes:', e);
              if (e.data) {
                console.error('[Editor] PocketBase error data:', e.data);
              }
              const toast = await toastController.create({
                message: `Failed to push changes: ${e.message}`,
                duration: 4000,
                color: 'danger'
              });
              await toast.present();
            }
          }
        }
      ]
    });

    await alert.present();

  } catch (e: any) {
    console.error('[Editor] Failed to load books:', e);
    const toast = await toastController.create({
      message: `Failed: ${e.message}`,
      duration: 3000,
      color: 'danger'
    });
    await toast.present();
  }
};

const getStatusColor = (status: string) => {
  switch (status) {
    case 'success': return 'success';
    case 'error': return 'danger';
    case 'pending': return 'warning';
    default: return 'medium';
  }
};

const onTableSelect = () => {
  if (selectedTable.value) {
    tableName.value = selectedTable.value;
  }
};

const clearResults = () => {
  tableName.value = "";
  selectedTable.value = "";
  results.value = null;
  error.value = "";
  lastQuery.value = "";
  resultsFilter.value = "";
};

const onCreateCollectionSelect = async () => {
  if (createSelectedCollection.value) {
    // Define fields and relations based on collection type
    const collectionSchemas: any = {
      menus: {
        fields: ['name', 'refId'],
        relations: {
          book: { type: 'single', collection: 'books' },
          books: { type: 'multi', collection: 'books' }
        }
      },
      offers: {
        fields: ['name', 'refId', 'multiplier'],
        relations: {
          book: { type: 'single', collection: 'books' },
          menus: { type: 'multi', collection: 'menus' },
          costsTime: { type: 'multi', collection: 'costsTime' },
          costsMaterial: { type: 'multi', collection: 'costsMaterial' }
        }
      },
      menuTiers: {
        fields: ['refId'],
        relations: {
          book: { type: 'single', collection: 'books' },
          menus: { type: 'multi', collection: 'menus' },
          offers: { type: 'multi', collection: 'offers' },
          tiers: { type: 'multi', collection: 'tiers' },
          menuCopy: { type: 'multi', collection: 'menuCopy' },
          contentItems: { type: 'multi', collection: 'contentItems' }
        }
      },
      contentItems: {
        fields: ['name', 'refId', 'content'],
        relations: {
          book: { type: 'single', collection: 'books' }
        }
      },
      menuCopy: {
        fields: ['name', 'refId'],
        relations: {
          book: { type: 'single', collection: 'books' },
          contentItems: { type: 'multi', collection: 'contentItems' }
        }
      },
      problems: {
        fields: ['name', 'refId', 'description'],
        relations: {
          book: { type: 'single', collection: 'books' },
          menus: { type: 'multi', collection: 'menus' },
          problemTags: { type: 'multi', collection: 'problemTags' }
        }
      },
      problemTags: {
        fields: ['name', 'refId'],
        relations: {
          book: { type: 'single', collection: 'books' }
        }
      },
      checklists: {
        fields: ['name', 'refId'],
        relations: {
          book: { type: 'single', collection: 'books' }
        }
      },
      costsMaterial: {
        fields: ['name', 'refId', 'quantity'],
        relations: {
          book: { type: 'single', collection: 'books' }
        }
      },
      costsTime: {
        fields: ['name', 'refId', 'hours'],
        relations: {
          book: { type: 'single', collection: 'books' }
        }
      },
      tierSets: {
        fields: ['name', 'refId'],
        relations: {
          book: { type: 'single', collection: 'books' }
        }
      },
      tiers: {
        fields: ['name', 'refId', 'rank'],
        relations: {
          book: { type: 'single', collection: 'books' },
          tierSets: { type: 'multi', collection: 'tierSets' }
        }
      },
      formulas: {
        fields: ['name', 'refId', 'formula'],
        relations: {
          book: { type: 'single', collection: 'books' }
        }
      },
      books: {
        fields: ['name', 'refId'],
        relations: {
          parent: { type: 'single', collection: 'books' },
          dependencies: { type: 'multi', collection: 'books' }
        }
      }
    };

    const schema = collectionSchemas[createSelectedCollection.value] || { fields: ['name', 'refId'], relations: {} };

    // Create field definitions
    createFields.value = schema.fields.map((fieldName: string) => {
      let type = 'text';
      let isCurrency = false;

      if (['quantity', 'multiplier', 'hours', 'rank'].includes(fieldName)) {
        type = 'number';
      }

      // Mark currency fields
      if (fieldName === 'quantity') {
        isCurrency = true;
      }

      return { name: fieldName, type, isRelation: false, isCurrency };
    });

    // Add relation fields
    for (const [relationName, relationConfig] of Object.entries(schema.relations)) {
      createFields.value.push({
        name: relationName,
        type: 'relation',
        isRelation: true,
        relationType: (relationConfig as any).type,
        relationCollection: (relationConfig as any).collection
      });
    }

    // Initialize empty data and relations
    createData.value = {};
    editedRelations.value = {};
    availableRelations.value = {};

    // Get the default book for this org (if book field exists)
    let defaultBookId = null;
    if (schema.relations.book) {
      try {
        const { pb } = await getApi();
        const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;
        if (activeOrg) {
          const books = await pb.collection('books').getList(1, 1, {
            filter: `org = '${activeOrg}'`,
            sort: '-created'
          });
          if (books.items.length > 0) {
            defaultBookId = books.items[0].id;
          }
        }
      } catch (error) {
        console.warn('Could not get default book:', error);
      }
    }

    for (const field of createFields.value) {
      if (field.isRelation) {
        // Set default book if this is the book field
        if (field.name === 'book' && defaultBookId) {
          editedRelations.value[field.name] = defaultBookId;
        } else {
          editedRelations.value[field.name] = field.relationType === 'single' ? null : [];
        }
        // Load available options for this relation
        availableRelations.value[field.name] = await loadAvailableRelations(field.relationCollection);
      } else {
        createData.value[field.name] = '';
      }
    }
  }
};

const cancelCreate = () => {
  createSelectedCollection.value = "";
  createFields.value = [];
  createData.value = {};
  goBackToBrowse();
};

const saveNewRecord = async () => {
  try {
    loading.value = true;

    // Get API and organization
    const { pb } = await getApi();
    const activeOrg = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;
    if (!activeOrg) {
      throw new Error('No active organization found');
    }

    // Get the default book for this org
    const books = await pb.collection('books').getList(1, 1, {
      filter: `org = '${activeOrg}'`,
      sort: '-created'
    });

    let bookId;
    if (books.items.length > 0) {
      bookId = books.items[0].id;
    } else {
      // Create a default book if none exists
      const newBook = await pb.collection('books').create({
        name: 'Editor Changes',
        refId: 'editorChanges',
        org: activeOrg
      });
      bookId = newBook.id;
    }

    // Prepare data with refs for relations
    const data: any = {};

    // Process regular fields and handle currency conversion
    for (const field of createFields.value) {
      if (!field.isRelation && createData.value[field.name] !== undefined) {
        let value = createData.value[field.name];

        // Convert currency fields from user's currency to base currency
        if (field.isCurrency && value !== '') {
          // Parse the value as a number first
          const numValue = parseFloat(value);
          if (!isNaN(numValue)) {
            try {
              value = convertToBaseCurrency(numValue);
            } catch (error) {
              console.warn(`Failed to convert currency for ${field.name}:`, error);
              // Keep the original value if conversion fails
              value = numValue;
            }
          }
        } else if (field.type === 'number' && value !== '') {
          // Convert other number fields to proper numbers
          value = parseFloat(value);
        }

        data[field.name] = value;
      }
    }

    // Process relations into refs structure (excluding book field)
    const refs: any[] = [];
    for (const [fieldName, value] of Object.entries(editedRelations.value)) {
      // Skip the book field - it's handled separately as a direct field
      if (fieldName === 'book') {
        continue;
      }

      if (value !== null && value !== undefined) {
        // Get the actual collection name from the field configuration
        const field = createFields.value.find(f => f.name === fieldName);
        const collectionName = field?.relationCollection || fieldName;

        // Handle single relations
        if (!Array.isArray(value)) {
          // For single relations, we need to get the refId
          const relationOption = availableRelations.value[fieldName]?.find((r: any) => r.value === value);
          if (relationOption?.refId) {
            refs.push({
              collection: collectionName,
              refIds: [relationOption.refId]
            });
          }
        }
        // Handle multi relations
        else if (value.length > 0) {
          const refIds = value.map((id: string) => {
            const relation = availableRelations.value[fieldName]?.find((r: any) => r.value === id);
            return relation?.refId || id;
          });
          refs.push({
            collection: collectionName,
            refIds: refIds
          });
        }
      }
    }

    // Add refs to data if there are any
    if (refs.length > 0) {
      data.refs = refs;
    }

    // Prepare changeset
    const changeset = [{
      collection: createSelectedCollection.value,
      operation: 'create',
      data: data
    }];

    // Submit the changeset
    const changesetRecord = await pb.collection('pricebookChanges').create({
      book: bookId,
      org: activeOrg,
      changeset: changeset
    });

    console.log('New record changeset submitted:', changesetRecord);

    // Show success message
    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: 'Record created successfully. Syncing database...',
      duration: 2000,
      color: 'success'
    });
    await toast.present();

    // Sync the database
    const { syncDatabase } = await import('@/dataAccess/getDb');
    const syncResult = await syncDatabase();

    if (syncResult.success) {
      const syncToast = await toastController.create({
        message: 'Database synced successfully',
        duration: 2000,
        color: 'success'
      });
      await syncToast.present();
    }

    // Clear create mode and go back
    cancelCreate();

  } catch (error: any) {
    console.error('Error creating record:', error);

    const { toastController } = await import('@ionic/vue');
    const toast = await toastController.create({
      message: `Failed to create record: ${error.message}`,
      duration: 4000,
      color: 'danger'
    });
    await toast.present();
  } finally {
    loading.value = false;
  }
};

const handleRouteChange = async () => {
  const id = route.params.id as string;
  const collection = route.params.collection as string;
  const path = route.path;

  if (path === '/editor/create') {
    isCreateMode.value = true;
    isIdMode.value = false;
    createSelectedCollection.value = "";
    createFields.value = [];
    createData.value = {};
  } else if (collection && id) {
    // Route with collection specified: /editor/:collection/:id
    searchId.value = id;
    isIdMode.value = true;
    isCreateMode.value = false;
    selectedTable.value = collection;
    await searchTableForId(collection, id);
  } else if (id && id !== 'create') {
    searchId.value = id;
    isIdMode.value = true;
    isCreateMode.value = false;
    searchAllTablesForId(id);
  } else {
    resetToInitialState();
  }
};

const searchTableForId = async (table: string, id: string) => {
  loading.value = true;
  error.value = "";
  // Clear single offer tech handbook when switching records
  singleOfferTechHandbook.value = null;
  singleOfferTechHandbookError.value = "";

  try {
    const sql = await getSQLite();
    const query = `SELECT * FROM ${table} WHERE id = '${id}'`;
    const result = await sql.dbConn.query(query);

    if (result.values && result.values.length > 0) {
      results.value = result;
      selectedTable.value = table;

      // If viewing an offer, also load its tech handbook
      if (table === 'offers') {
        loadSingleOfferTechHandbook(id);
      }
    } else {
      error.value = `No record found with ID: ${id} in table: ${table}`;
      results.value = null;
    }
  } catch (e: any) {
    error.value = e.message || 'Query failed';
    results.value = null;
  } finally {
    loading.value = false;
  }
};

// Watch for route changes
watch(() => [route.params.id, route.params.collection], handleRouteChange, { immediate: true });

// Watch for dark mode toggles to activate super secret mode
watch(() => preferences.darkModeToggleCount, (newCount) => {
  // Only count toggles while in secret mode and viewing change history in JSON mode
  if (sessionStore.secretMode && showChangeHistory.value && changeHistoryViewMode.value === 'json') {
    const togglesSinceLastCheck = newCount - lastToggleCount.value;
    if (togglesSinceLastCheck >= 4) {
      superSecretMode.value = true;
      preferences.resetDarkModeToggleCount();
      lastToggleCount.value = 0;
    }
  } else {
    // Reset counter when not in the right mode
    lastToggleCount.value = newCount;
  }
});

// Reset super secret mode when leaving change history or JSON view
watch([showChangeHistory, changeHistoryViewMode], () => {
  if (!showChangeHistory.value || changeHistoryViewMode.value !== 'json') {
    superSecretMode.value = false;
    lastToggleCount.value = preferences.darkModeToggleCount;
  }
});

onMounted(async () => {
  handleRouteChange();

  // Check for ?secretmode=true URL parameter to enable super secret mode
  const urlParams = new URLSearchParams(window.location.search);
  if (urlParams.get('secretmode') === 'true') {
    // Enable the same state as toggling dark mode 4x in secret mode
    showChangeHistory.value = true;
    changeHistoryViewMode.value = 'json';
    superSecretMode.value = true;
    await loadChangeMessages();
  }

  // Load books cache
  try {
    const { dbConn } = await getSQLite();
    const booksResult = await dbConn.query('SELECT id, name FROM books');
    if (booksResult.values) {
      booksCache.value = booksResult.values;
    }
  } catch (error) {
    console.warn('Failed to load books cache:', error);
  }
});
</script>

<style scoped>
.lookup-row {
  margin-bottom: 12px;
  align-items: center;
}

.lookup-input {
  --padding-start: 12px;
  --padding-end: 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
  min-width: 250px;
}

.results-cards {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.record-card {
  margin: 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.field-row {
  display: flex;
  flex-direction: column;
  margin-bottom: 8px;
  padding: 4px 0;
  border-bottom: 1px solid #f0f0f0;
}

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

.field-row strong {
  font-size: 12px;
  color: #666;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 2px;
}

.field-value {
  font-family: monospace;
  font-size: 14px;
  color: #333;
  word-break: break-word;
  line-height: 1.4;
}

.relation-data {
  margin-top: 8px;
}

.related-item {
  background-color: #f8f9fa;
  border: 1px solid #e9ecef;
  border-radius: 4px;
  padding: 8px;
  margin-bottom: 8px;
}

.related-item:last-child {
  margin-bottom: 0;
}

.related-header {
  font-weight: bold;
  color: #495057;
  font-size: 12px;
  margin-bottom: 6px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.related-field {
  display: flex;
  flex-direction: column;
  margin-bottom: 4px;
  padding-bottom: 4px;
  border-bottom: 1px solid #dee2e6;
}

.related-field:last-child {
  border-bottom: none;
  margin-bottom: 0;
}

.related-key {
  font-size: 11px;
  color: #6c757d;
  font-weight: bold;
  margin-bottom: 2px;
}

.related-value {
  font-family: monospace;
  font-size: 12px;
  color: #212529;
}

pre {
  white-space: pre-wrap;
  word-wrap: break-word;
}

.table-select {
  width: 100%;
  padding: 12px 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background-color: white;
  font-size: 14px;
  color: #333;
}

.id-link {
  color: #3880ff;
  text-decoration: underline;
  cursor: pointer;
  font-family: monospace;
}

.id-link:hover {
  color: #0066cc;
  text-decoration: none;
}

.edit-input {
  margin-top: 4px;
  font-family: monospace;
  width: 100%;
  min-width: 300px;
}

.edit-input ion-input,
.edit-input ion-textarea {
  --padding-start: 12px;
  --padding-end: 12px;
  --padding-top: 8px;
  --padding-bottom: 8px;
  font-family: monospace;
}

.currency-hint {
  display: block;
  margin-top: 4px;
  color: #666;
  font-style: italic;
  font-size: 12px;
}

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

.relation-edit-header {
  font-weight: bold;
  margin-bottom: 8px;
  color: #495057;
}

.current-relations {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

.relation-chip.removable {
  cursor: pointer;
  background-color: #e9ecef;
  transition: background-color 0.2s;
}

.relation-chip.removable:hover {
  background-color: #f8d7da;
}

.remove-icon {
  margin-left: 4px;
  font-size: 14px;
}

.add-relations {
  margin-top: 8px;
}

.relation-add-button {
  width: 100%;
  margin-top: 4px;
}

.relation-modal-content {
  --padding-bottom: 0;
}

.relation-search-input {
  margin-bottom: 16px;
}

.relation-options {
  overflow-y: auto;
  padding-bottom: 16px;
}

.relation-option-item {
  --padding-start: 16px;
  --padding-end: 16px;
}

.no-results {
  text-align: center;
  color: #666;
  padding: 20px;
  font-style: italic;
}

.relation-selector-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  padding: 12px 16px;
  width: 100%;
}

.book-info {
  padding: 12px;
  background-color: #f0f4f8;
  border-radius: 8px;
  margin-bottom: 16px;
  font-size: 15px;
}

.changeset-card {
  margin-bottom: 12px;
}

.changeset-data {
  margin-top: 8px;
}

.data-field {
  display: flex;
  flex-direction: column;
  margin-bottom: 8px;
  padding: 8px;
  background-color: #f8f9fa;
  border-left: 3px solid #007bff;
  border-radius: 4px;
}

.data-field strong {
  font-size: 12px;
  color: #666;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 4px;
}

.data-value {
  font-family: monospace;
  font-size: 14px;
  color: #333;
  word-break: break-word;
  line-height: 1.4;
}

.changeset-modal-content {
  --padding-bottom: 0;
}

.changeset-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  padding: 12px 16px;
  width: 100%;
}

.create-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid #eee;
}

.filter-container {
  margin-bottom: 16px;
}

.filter-container ion-item {
  --background: #f8f9fa;
  --border-radius: 8px;
  --padding-start: 12px;
  --padding-end: 12px;
}

.warning-card {
  background-color: #fff5f5;
  border: 1px solid #feb2b2;
}

.warning-text {
  color: #e53e3e;
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
  font-weight: 600;
}

@media (min-width: 768px) {
  .field-row {
    flex-direction: row;
    align-items: flex-start;
  }

  .field-row strong {
    min-width: 120px;
    margin-bottom: 0;
    margin-right: 12px;
    margin-top: 8px;
  }

  .field-value {
    flex: 1;
    min-width: 0;
  }
}

/* Change messages */
.change-message-card {
  margin-bottom: 12px;
}

.change-message-card ion-card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.changeset-summary ul {
  margin: 8px 0 0 16px;
  padding: 0;
}

.changeset-summary li {
  margin-bottom: 4px;
  font-family: monospace;
  font-size: 13px;
}

.error-message {
  background-color: #fee2e2;
  color: #dc2626;
  padding: 8px 12px;
  border-radius: 4px;
  margin-bottom: 12px;
  font-size: 13px;
}

.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: #666;
}

/* Change History Styles */
.change-history-section {
  margin-top: 16px;
}

.view-mode-segment {
  margin-top: 12px;
  max-width: 300px;
}

.friendly-view {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.change-header-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

.change-meta {
  margin-top: 8px;
  font-size: 13px;
  color: #666;
}

.meta-item {
  margin-right: 16px;
}

.changeset-details {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.change-item {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 12px;
  border-left: 4px solid #6c757d;
}

.change-item:has(.change-operation.create) {
  border-left-color: #28a745;
}

.change-item:has(.change-operation.update) {
  border-left-color: #007bff;
}

.change-item:has(.change-operation.delete) {
  border-left-color: #dc3545;
}

.change-operation {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.op-badge {
  font-size: 11px;
  font-weight: bold;
  padding: 2px 8px;
  border-radius: 4px;
  background: #6c757d;
  color: white;
}

.op-badge.create, .change-operation.create .op-badge {
  background: #28a745;
}

.op-badge.update, .change-operation.update .op-badge {
  background: #007bff;
}

.op-badge.delete, .change-operation.delete .op-badge {
  background: #dc3545;
}

.collection-name {
  font-weight: 600;
  color: #333;
}

.ref-id {
  font-family: monospace;
  font-size: 12px;
  color: #666;
  background: #e9ecef;
  padding: 2px 6px;
  border-radius: 4px;
}

.change-data-preview {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}

.data-preview-field {
  font-size: 12px;
  background: white;
  padding: 4px 8px;
  border-radius: 4px;
  border: 1px solid #dee2e6;
}

.field-key {
  color: #666;
  margin-right: 4px;
}

.field-value-preview {
  font-family: monospace;
  color: #333;
}

/* JSON View Styles */
.json-view {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.json-controls {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 8px;
}

.json-message-block {
  border: 1px solid #dee2e6;
  border-radius: 8px;
  overflow: hidden;
}

.json-message-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  background: #f8f9fa;
  border-bottom: 1px solid #dee2e6;
}

.json-date {
  font-size: 13px;
  color: #666;
  flex: 1;
}

.json-editor {
  width: 100%;
  min-height: 200px;
  padding: 12px;
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
  font-size: 13px;
  line-height: 1.5;
  border: none;
  resize: vertical;
  background: #1e1e1e;
  color: #d4d4d4;
}

.json-editor:focus {
  outline: none;
  box-shadow: inset 0 0 0 2px #007bff;
}

.json-error {
  padding: 8px 12px;
  background: #fee2e2;
  color: #dc2626;
  font-size: 12px;
  font-family: monospace;
}

/* Change Detail Modal */
.detail-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid #dee2e6;
}

.detail-data h4 {
  margin: 0 0 12px 0;
  color: #333;
}

.detail-field {
  margin-bottom: 12px;
  padding: 8px;
  background: #f8f9fa;
  border-radius: 4px;
}

.detail-field strong {
  display: block;
  font-size: 12px;
  color: #666;
  text-transform: uppercase;
  margin-bottom: 4px;
}

.detail-value {
  font-family: monospace;
  font-size: 13px;
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Save Confirmation Dialog */
.confirm-dialog-content {
  text-align: center;
  padding: 20px;
}

.confirm-icon {
  font-size: 64px;
  margin-bottom: 16px;
}

.confirm-dialog-content h3 {
  margin: 0 0 12px 0;
  color: #333;
}

.confirm-dialog-content p {
  color: #666;
  margin-bottom: 12px;
}

.confirm-dialog-content .db-filename {
  font-size: 14px;
  color: #333;
  margin-bottom: 16px;
}

.confirm-dialog-content .db-filename code {
  background: #f5f5f5;
  padding: 4px 8px;
  border-radius: 4px;
  font-family: monospace;
  color: #d32f2f;
}

.confirm-note {
  background: #fff8e1;
  border: 1px solid #ffcc02;
  border-radius: 8px;
  padding: 12px;
  font-size: 13px;
  text-align: left;
}

.confirm-details {
  background: #f8f9fa;
  border-radius: 8px;
  padding: 12px;
  margin-top: 16px;
  text-align: left;
  font-size: 13px;
  font-family: monospace;
}

.confirm-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  padding: 12px 16px;
  width: 100%;
}

/* Replay Dialog Styles */
.replay-steps {
  text-align: left;
  margin: 16px 0;
  padding-left: 24px;
}

.replay-steps li {
  margin-bottom: 8px;
  color: #333;
}

.replay-warning {
  background: #fee2e2;
  border: 1px solid #fca5a5;
  border-radius: 8px;
  padding: 12px;
  margin-top: 16px;
  text-align: left;
  font-size: 13px;
  display: flex;
  gap: 8px;
  align-items: flex-start;
}

.replay-warning ion-icon {
  font-size: 20px;
  flex-shrink: 0;
  margin-top: 2px;
}

/* Replay Progress Overlay */
.replay-progress-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 99999;
}

.replay-progress-card {
  width: 90%;
  max-width: 400px;
  text-align: center;
}

.replay-progress-card ion-spinner {
  font-size: 48px;
  margin-bottom: 16px;
}

.replay-progress-card h3 {
  margin: 0 0 8px 0;
  color: #333;
}

.replay-progress-card p {
  color: #666;
  margin: 8px 0;
}

.progress-bar {
  width: 100%;
  height: 8px;
  background: #e0e0e0;
  border-radius: 4px;
  overflow: hidden;
  margin: 16px 0;
}

.progress-fill {
  height: 100%;
  background: #3880ff;
  transition: width 0.3s ease;
}

.progress-count {
  font-family: monospace;
  font-size: 14px;
  color: #666;
}

/* Replay Results Dialog */
.replay-results-summary {
  display: flex;
  justify-content: center;
  gap: 32px;
  margin-bottom: 24px;
  padding: 16px;
  background: #f8f9fa;
  border-radius: 12px;
}

.result-stat {
  text-align: center;
}

.stat-number {
  display: block;
  font-size: 36px;
  font-weight: bold;
  line-height: 1;
}

.stat-label {
  font-size: 14px;
  color: #666;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.result-stat.success .stat-number {
  color: #28a745;
}

.result-stat.error .stat-number {
  color: #dc3545;
}

.replay-errors-section h3 {
  margin: 0 0 8px 0;
  color: #333;
}

.errors-hint {
  color: #666;
  font-size: 14px;
  margin-bottom: 16px;
}

.replay-error-item {
  background: #fff;
  border: 1px solid #fee2e2;
  border-left: 4px solid #dc3545;
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 12px;
}

.error-header {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  font-size: 12px;
  color: #666;
  margin-bottom: 8px;
}

.error-id {
  font-family: monospace;
  background: #e9ecef;
  padding: 2px 6px;
  border-radius: 4px;
}

.error-date {
  color: #666;
}

.error-book {
  color: #666;
}

.error-message {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  background: #fee2e2;
  color: #dc3545;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 13px;
  margin-bottom: 8px;
}

.error-message ion-icon {
  flex-shrink: 0;
  font-size: 18px;
  margin-top: 1px;
}

.error-changeset {
  margin-top: 8px;
}

.error-changeset strong {
  display: block;
  font-size: 12px;
  color: #666;
  margin-bottom: 4px;
}

.error-changeset pre {
  background: #1e1e1e;
  color: #d4d4d4;
  padding: 12px;
  border-radius: 6px;
  font-size: 11px;
  overflow-x: auto;
  max-height: 200px;
  margin: 0;
}

/* Super secret mode / Delete database styles */
.skull-icon {
  color: #333;
}

.pull-info {
  background: #e3f2fd;
  border: 1px solid #90caf9;
  border-radius: 8px;
  padding: 12px;
  margin-top: 16px;
  text-align: left;
  font-size: 13px;
}

.pull-info p {
  margin: 0 0 8px 0;
  color: #1565c0;
  font-weight: 500;
}

.pull-info ul {
  margin: 0;
  padding-left: 20px;
  color: #333;
}

.pull-info li {
  margin-bottom: 4px;
}

/* DB File Picker Styles */
.db-picker-content {
  padding: 8px;
}

.db-picker-info {
  color: #666;
  font-size: 14px;
  margin-bottom: 20px;
  padding: 12px;
  background: #f5f5f5;
  border-radius: 8px;
}

.db-loading {
  text-align: center;
  padding: 40px;
}

.db-loading ion-spinner {
  font-size: 32px;
  margin-bottom: 12px;
}

.db-loading p {
  color: #666;
}

.db-empty {
  text-align: center;
  padding: 40px;
  color: #666;
}

.db-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.db-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  background: #f8f9fa;
  border: 1px solid #dee2e6;
  border-radius: 8px;
}

.db-item-info {
  display: flex;
  align-items: center;
  gap: 12px;
}

.db-icon {
  font-size: 24px;
  color: #666;
}

.db-details {
  display: flex;
  flex-direction: column;
}

.db-name {
  font-weight: 600;
  font-family: monospace;
  font-size: 14px;
  color: #333;
}

.db-version {
  font-size: 12px;
  color: #666;
}

.db-item-actions {
  display: flex;
  gap: 8px;
}

.db-picker-actions {
  margin-top: 24px;
  display: flex;
  justify-content: center;
}

.db-section {
  margin-bottom: 32px;
}

.db-section-title {
  margin: 0 0 8px 0;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}

.db-section-info {
  color: #666;
  font-size: 13px;
  margin-bottom: 16px;
}

.db-empty-inline {
  padding: 16px;
  text-align: center;
  color: #999;
  font-style: italic;
  background: #fafafa;
  border-radius: 8px;
}

.db-empty-inline p {
  margin: 0;
}

.sqlite-item {
  border-left: 4px solid #3880ff;
}

.sqlite-icon {
  color: #3880ff;
}

.db-type {
  font-size: 11px;
  color: #3880ff;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* Download All Changes Modal */
.loading-state,
.error-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px;
  gap: 16px;
}

.error-state {
  color: var(--ion-color-danger);
}

.changes-summary {
  margin-bottom: 16px;
  font-size: 14px;
}

.downloaded-changes-controls {
  margin-bottom: 16px;
}

.downloaded-changes-code {
  background: #1e1e1e;
  color: #d4d4d4;
  padding: 16px;
  border-radius: 8px;
  overflow: auto;
  max-height: 60vh;
  font-size: 12px;
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Tech Handbook Browser */
.tech-handbook-section {
  margin-top: 16px;
  margin-bottom: 16px;
}

.tech-handbook-summary {
  margin-bottom: 16px;
  font-size: 14px;
}

.tech-handbook-offer {
  margin-bottom: 24px;
  padding: 16px;
  background: var(--ion-color-light);
  border-radius: 8px;
}

.tech-handbook-offer .offer-title {
  margin: 0 0 12px 0;
  font-size: 18px;
  color: var(--ion-color-primary);
  border-bottom: 1px solid var(--ion-color-medium);
  padding-bottom: 8px;
}

.tech-handbook-items {
  margin: 0;
  padding-left: 20px;
}

.tech-handbook-item {
  margin-bottom: 8px;
  line-height: 1.5;
}

.tech-handbook-item strong {
  color: var(--ion-color-dark);
}

.empty-state {
  text-align: center;
  padding: 20px;
  color: var(--ion-color-medium);
}
</style>