<template>
  <fluent-design-system-provider accent-base-color="#2457a6" :base-layer-luminance="fluentLuminance">
    <v-app :theme="activeTheme">
      <LoginView v-if="!token" :error="error" :form="loginForm" :loading="loading" @login="login" />

      <div v-else-if="forcePasswordChange" class="forced-change-screen">
        <ChangePasswordCard :token="token" forced standalone @changed="completeForcedChange" @cancel="logout" />
      </div>

      <div v-else class="app-shell">
        <a class="skip-link" href="#main-content">Skip to main content</a>
        <v-navigation-drawer class="rail" :width="254" :rail="navCollapsed" :rail-width="76" permanent aria-label="Primary navigation">
          <div class="brand-lockup">
            <template v-if="!navCollapsed">
              <span class="brand-mark">MA</span>
              <span class="brand-name">DepreCore</span>
              <v-spacer />
              <v-btn icon="mdi-chevron-left" size="small" variant="text" title="Collapse menu" aria-label="Collapse navigation menu" @click="toggleNav" />
            </template>
            <v-btn v-else icon="mdi-menu" size="small" variant="text" class="mx-auto" title="Expand menu" aria-label="Expand navigation menu" @click="toggleNav" />
          </div>
          <v-divider />
          <v-list nav density="compact" class="pa-2">
            <template v-for="item in visibleNavItems" :key="item.key">
              <v-list-group v-if="item.children && item.children.length" :value="item.key">
                <template #activator="{ props }">
                  <v-list-item
                    v-bind="props"
                    :active="isNavActive(item)"
                    :prepend-icon="item.icon"
                    :title="item.label"
                    rounded="sm"
                    @click="selectNav(item)"
                  />
                </template>
                <v-list-item
                  v-for="child in item.children"
                  :key="child.key"
                  :active="activeView === child.key"
                  :prepend-icon="child.icon"
                  :title="child.label"
                  rounded="sm"
                  @click="activeView = child.key"
                />
              </v-list-group>
              <v-list-item
                v-else
                :active="activeView === item.key"
                :prepend-icon="item.icon"
                :title="item.label"
                rounded="sm"
                @click="activeView = item.key"
              />
            </template>
          </v-list>
          <template #append>
            <div :class="navCollapsed ? 'pa-2 text-center' : 'pa-4'">
              <template v-if="!navCollapsed">
                <div class="text-body-2 font-weight-bold">{{ user?.name }}</div>
                <div class="text-caption quiet">{{ user?.role }}</div>
                <v-btn class="mt-3" block variant="tonal" prepend-icon="mdi-logout" @click="logout">Sign out</v-btn>
              </template>
              <v-btn v-else icon="mdi-logout" size="small" variant="text" title="Sign out" aria-label="Sign out" @click="logout" />
            </div>
          </template>
        </v-navigation-drawer>

        <TopNav
          :preferences="profilePreferences"
          :saving="preferenceSaving"
          :subtitle="currentSubtitle"
          :theme-items="themeItems"
          :title="currentTitle"
          :user="user"
          :companies="entities"
          :active-company-id="activeCompanyId"
          @change-company="changeCompany"
          @change-password="changePasswordDialog = true"
          @logout="logout"
          @open-help="helpDialog = true"
          @save-preferences="saveProfilePreferences"
          @toggle-nav="toggleNav"
        >
          <template #actions>
              <v-btn v-if="activeView === 'assets' && canEditAssets" color="primary" prepend-icon="mdi-plus" @click="newAsset">New asset</v-btn>
          </template>
        </TopNav>

        <!-- Keying on the active company remounts the whole view tree on a company switch, so each
             screen re-fetches its data scoped to the newly selected company. -->
        <v-main id="main-content" :key="`company-${activeCompanyId}`" tabindex="-1">
          <DashboardView
            v-if="activeView === 'dashboard'"
            :token="token"
            @navigate="activeView = $event"
          />
          <AssetsView
            v-if="activeView === 'assets'"
            :all-selected="allSelected"
            :assets="assets"
            :asset-filters="assetFilters"
            :currency="currency"
            :selected-asset-ids="selectedAssetIds"
            :status-items="statusItems"
            @edit-asset="editAsset"
            @export-assets="exportAssets"
            @filter-assets="filterAssets"
            @import-assets="importAssets"
            @open-mass-edit="massDialog = true"
            @print-labels="printLabels"
            @toggle-all="toggleAll"
            @toggle-asset="toggleAsset"
          />
          <ScanView
            v-if="activeView === 'scan'"
            :token="token"
            @asset-updated="onScanAssetUpdated"
          />
          <AlertsView
            v-if="activeView === 'alerts'"
            :token="token"
            :is-admin="user?.role === 'admin'"
          />
          <PmView
            v-if="activeView === 'pm'"
            :token="token"
          />
          <PartsView
            v-if="activeView === 'parts'"
            :token="token"
            :can-delete="canDeleteParts"
          />
          <ContactsView
            v-if="activeView === 'contacts'"
            :token="token"
            :can-delete="canManageContacts"
          />
          <AssignmentsView
            v-if="activeView === 'assignments'"
            :assignments="assignments"
            :assets="assets"
            :assignment-saving="assignmentSaving"
            :employees="employees"
            :short-date="shortDate"
            :summary="assignmentSummary"
            @add-employee="addEmployee"
            @assign-asset="assignAsset"
            @release-assignment="releaseAssignment"
            @reload="loadAssignments"
          />
          <ReportsView
            v-if="activeView === 'reports'"
            :token="token"
          />
          <TaxRulesView
            v-if="activeView === 'tax-rules'"
            :token="token"
            :dark="activeTheme === 'depreCoreDark'"
            @updated="reloadTaxRules"
          />
          <BooksView
            v-if="activeView === 'books'"
            :token="token"
            :method-items="methodItems"
            @updated="loadBookCodes"
          />
          <GlJournalsView
            v-if="activeView === 'gl-journals'"
            :token="token"
          />
          <CloseView
            v-if="activeView === 'close'"
            :token="token"
          />
          <Section179View
            v-if="activeView === 'section-179'"
            :token="token"
          />
          <TemplatesView
            v-if="activeView === 'templates'"
            :category-items="categoryItems"
            :custom-field-types="customFieldTypes"
            :method-items="methodItems"
            :template-form="templateForm"
            :templates="templates"
            :can-delete="canManageConfig"
            @save-template="saveTemplate"
            @delete-template="deleteTemplate"
          />
          <AdminView
            v-if="activeView.startsWith('admin')"
            :section="activeView"
            :token="token"
            :capability-catalog="capabilityCatalog"
            :roles="roles"
            :short-date="shortDate"
            :settings="settings"
            :tax-rules="taxRules"
            :team-saving="teamSaving"
            :team-notice="teamNotice"
            :teams="teams"
            :companies="entities"
            :user-saving="userSaving"
            :users="users"
            :workday-connection="workdayConnection"
            :workday-saving="workdaySaving"
            :workday-syncing="workdaySyncing"
            @categories-updated="loadReferences"
            @companies-updated="loadCompanies"
            @create-team="createTeam"
            @update-team="updateTeam"
            @delete-team="deleteTeam"
            @create-user="createUser"
            @create-role="createRole"
            @update-role="updateRole"
            @delete-role="deleteRole"
            @save-settings="saveSettings"
            @save-workday="saveWorkdayConnection"
            @sync-workday="syncWorkday"
            @update-user="updateUser"
            @unlock-user="unlockUser"
          />
        </v-main>

        <AssetDrawer
          v-model="assetDrawer"
          :asset-form="assetForm"
          :asset-adjustments="activeAdjustments"
          :asset-disposals="activeDisposals"
          :asset-files="activeFiles"
          :asset-leases="activeLeases"
          :asset-notes="activeNotes"
          :asset-photos="activePhotos"
          :asset-pm="activePm"
          :asset-tasks="activeTasks"
          :asset-warranties="activeWarranties"
          :book-items="activeBookItems"
          :pm-plans="pmPlans"
          :pm-defaults="pmDefaults"
          :current-user-name="user?.name"
          :category-items="categoryItems"
          :disposal-preview="disposalPreview"
          :drawer-error="drawerError"
          :entity-options="entityOptions"
          :location-items="locationItems"
          :department-items="departmentItems"
          :vendor-items="vendorItems"
          :category-class-numbers="categoryClassNumbers"
          :zone-items="zoneItems"
          :zone-details="zoneDetails"
          :lease-schedule="leaseSchedule"
          :method-items="methodItems"
          :saving="saving"
          :selected-lease-id="selectedLeaseId"
          :selected-template-fields="selectedTemplateFields"
          :selected-template-id="selectedTemplateId"
          :status-items="statusItems"
          :template-options="templateOptions"
          @attach-pm="attachPm"
          @detach-pm="detachPm"
          @complete-pm="completePm"
          @log-reading="logReading"
          @view-completion="viewCompletion"
          @add-adjustment="addAdjustment"
          @add-note="addNote"
          @add-task="addTask"
          @upload-photo="uploadAssetPhoto"
          @update-photo="updateAssetPhoto"
          @delete-photo="deleteAssetPhoto"
          @add-warranty="addWarranty"
          @delete-adjustment="deleteAdjustment"
          @delete-file="deleteAssetFile"
          @delete-lease="deleteLease"
          @delete-note="deleteNote"
          @delete-task="deleteTask"
          @delete-warranty="deleteWarranty"
          @download-file="downloadAssetFile"
          @load-lease-schedule="loadLeaseSchedule"
          @preview-disposal="previewDisposal"
          @print-label="printSingleLabel"
          @record-disposal="recordDisposal"
          @reverse-disposal="reverseDisposal"
          @save-asset="saveAsset"
          @save-lease="saveLease"
          @select-template="applyTemplate"
          @toggle-task="toggleTask"
          @upload-file="uploadAssetFile"
        />

        <MassEditDialog
          v-model="massDialog"
          :changes="massChanges"
          :status-items="statusItems"
          @apply="massUpdate"
        />

        <LabelSheet v-model="labelDialog" :assets="labelAssets" />

        <PmCompletionDialog v-model="completionDialog" :token="token" :completion-id="completionId" />

        <UserManual v-model="helpDialog" />

        <v-dialog v-model="changePasswordDialog" max-width="480">
          <ChangePasswordCard :token="token" @changed="changePasswordDialog = false" @cancel="changePasswordDialog = false" />
        </v-dialog>

        <v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="5000" location="top end">
          {{ snackbar.text }}
        </v-snackbar>
      </div>
    </v-app>
  </fluent-design-system-provider>
</template>

<script>
import AdminView from './views/AdminView.vue';
import AlertsView from './views/AlertsView.vue';
import AssignmentsView from './views/AssignmentsView.vue';
import BooksView from './views/BooksView.vue';
import GlJournalsView from './views/GlJournalsView.vue';
import CloseView from './views/CloseView.vue';
import Section179View from './views/Section179View.vue';
import ContactsView from './views/ContactsView.vue';
import AssetsView from './views/AssetsView.vue';
import DashboardView from './views/DashboardView.vue';
import LoginView from './views/LoginView.vue';
import ChangePasswordCard from './components/ChangePasswordCard.vue';
import PartsView from './views/PartsView.vue';
import PmView from './views/PmView.vue';
import ReportsView from './views/ReportsView.vue';
import ScanView from './views/ScanView.vue';
import TaxRulesView from './views/TaxRulesView.vue';
import TemplatesView from './views/TemplatesView.vue';
import AssetDrawer from './components/AssetDrawer.vue';
import LabelSheet from './components/LabelSheet.vue';
import MassEditDialog from './components/MassEditDialog.vue';
import PmCompletionDialog from './components/PmCompletionDialog.vue';
import TopNav from './components/TopNav.vue';
import UserManual from './components/UserManual.vue';
import { apiRequest, loginRequest, saveBlob, setActiveCompany } from './services/api';
import { blankAsset, defaultValueForField, makeDefaultBooks, mergeBooks, normalizeCustomFields } from './utils/assets';
import { currency, shortDate } from './utils/format';
import { bookItems, customFieldTypes, darkThemes, fontScaleMap, navItems, statusItems, themeItems } from './constants';

export default {
  components: {
    AdminView,
    AlertsView,
    AssignmentsView,
    AssetDrawer,
    AssetsView,
    BooksView,
    GlJournalsView,
    CloseView,
    Section179View,
    ChangePasswordCard,
    ContactsView,
    DashboardView,
    LabelSheet,
    LoginView,
    MassEditDialog,
    PartsView,
    PmCompletionDialog,
    PmView,
    ReportsView,
    ScanView,
    TaxRulesView,
    TemplatesView,
    TopNav,
    UserManual
  },
  data() {
    return {
      activeView: 'dashboard',
      activeAdjustments: [],
      activeDisposals: [],
      activeFiles: [],
      activeLeases: [],
      activeNotes: [],
      activePhotos: [],
      activePm: [],
      activeTasks: [],
      activeWarranties: [],
      assets: [],
      bookCodes: [],
      completionDialog: false,
      completionId: null,
      pmPlans: [],
      pmDefaults: { value: 3, unit: 'months' },
      assetDrawer: false,
      assetFilters: { search: '', status: null },
      assetForm: blankAsset(),
      assignments: [],
      assignmentSaving: false,
      assignmentSummary: {},
      bookItems,
      customFieldTypes,
      disposalPreview: null,
      drawerError: '',
      leaseSchedule: null,
      selectedLeaseId: null,
      employees: [],
      entities: [],
      activeCompanyId: null,
      error: '',
      helpDialog: false,
      changePasswordDialog: false,
      snackbar: { show: false, text: '', color: 'error' },
      labelDialog: false,
      labelAssetsOverride: null,
      loading: false,
      forcePasswordChange: false,
      loginForm: { email: 'admin@deprecore.local', password: 'ChangeMe123!' },
      massChanges: { status: null, location: '', department: '' },
      massDialog: false,
      navCollapsed: localStorage.getItem('deprecore.navCollapsed') === 'true',
      navItems,
      preferenceSaving: false,
      profilePreferences: {
        theme: localStorage.getItem('deprecore.theme') || 'depreCoreDark',
        fontScale: localStorage.getItem('deprecore.fontScale') || 'normal',
        accent: localStorage.getItem('deprecore.accent') || '',
        contrast: localStorage.getItem('deprecore.contrast') === 'true'
      },
      references: [],
      workLocations: [],
      vendorContacts: [],
      depreciationZones: [],
      userCapabilities: JSON.parse(localStorage.getItem('deprecore.capabilities') || '[]'),
      capabilityCatalog: [],
      roles: [],
      saving: false,
      selectedAssetIds: [],
      selectedTemplateId: null,
      settings: {},
      statusItems,
      taxRules: [],
      teamSaving: false,
      teamNotice: '',
      teams: [],
      templateForm: {
        id: null,
        name: '',
        description: '',
        defaults: { category: 'Computer Equipment', useful_life_months: 60, depreciation_method: 'straight_line' },
        custom_fields: []
      },
      templates: [],
      themeItems,
      token: localStorage.getItem('deprecore.token') || '',
      user: JSON.parse(localStorage.getItem('deprecore.user') || 'null'),
      userSaving: false,
      users: [],
      workdayConnection: {},
      workdaySaving: false,
      workdaySyncing: false
    };
  },
  computed: {
    allSelected() {
      return this.assets.length > 0 && this.selectedAssetIds.length === this.assets.length;
    },
    categoryItems() {
      // Admin-managed categories, plus any value already in use so existing assets stay selectable.
      const managed = this.references.filter((item) => item.type === 'category').map((item) => item.name);
      const inUse = this.assets.map((asset) => asset.category).filter(Boolean);
      return [...new Set([...managed, ...inUse])].sort((a, b) => String(a).localeCompare(String(b)));
    },
    locationItems() {
      // Work-location contacts, plus any location value already in use so existing assets stay selectable.
      const inUse = this.assets.map((asset) => asset.location).filter(Boolean);
      return [...new Set([...this.workLocations, ...inUse])].sort((a, b) => String(a).localeCompare(String(b)));
    },
    departmentItems() {
      // Admin-managed departments, plus any value already in use so existing assets stay selectable.
      const managed = this.references.filter((item) => item.type === 'department').map((item) => item.name);
      const inUse = this.assets.map((asset) => asset.department).filter(Boolean);
      return [...new Set([...managed, ...inUse])].sort((a, b) => String(a).localeCompare(String(b)));
    },
    categoryClassNumbers() {
      // Map of category name -> asset class number, for live display on the asset form.
      const map = {};
      for (const ref of this.references) {
        if (ref.type === 'category' && ref.metadata && ref.metadata.asset_class_number) {
          map[ref.name] = ref.metadata.asset_class_number;
        }
      }
      return map;
    },
    vendorItems() {
      // Vendor / supplier contacts, plus any vendor value already in use so existing assets stay selectable.
      const inUse = this.assets.map((asset) => asset.vendor).filter(Boolean);
      return [...new Set([...this.vendorContacts, ...inUse])].sort((a, b) => String(a).localeCompare(String(b)));
    },
    zoneItems() {
      return [{ title: 'None', value: '' }, ...this.depreciationZones.filter((z) => z.enabled).map((z) => ({ title: z.name, value: z.code }))];
    },
    zoneDetails() {
      return Object.fromEntries(this.depreciationZones.map((z) => [z.code, z]));
    },
    canEditAssets() {
      return this.can('assets.edit');
    },
    canDeleteParts() {
      return this.can('parts.manage');
    },
    canManageContacts() {
      return this.can('contacts.manage');
    },
    canManageConfig() {
      return this.can('config.manage');
    },
    currentSubtitle() {
      return {
        dashboard: 'Portfolio value, lifecycle activity, and open work',
        assets: 'Asset register, imports, exports, and lifecycle updates',
        scan: 'Scan a barcode to find and update an asset in the field',
        alerts: 'Maintenance, warranty, and lease reminders with email digests',
        pm: 'Build reusable preventative maintenance plans with step checklists',
        parts: 'Maintenance parts inventory — stock levels, aisle/bin locations, suppliers, and reservations',
        assignments: 'Assign assets to people, departments, and locations with full custody history',
        contacts: 'Employees, vendors, service providers, contractors, and work locations',
        reports: 'Depreciation schedules, monthly expense, and book values',
        books: 'Configure books, assign tax rule sets, and set entry defaults',
        'gl-journals': 'General ledger rollup and editable journal entries per book',
        close: 'Guided month-end / year-end close: post, reconcile, and lock the period',
        'section-179': 'Section 179 deduction limitation, taxable-income cap, and carryover',
        templates: 'JSON-driven entry defaults and custom fields',
        'tax-rules': 'Upload, edit, export, and activate depreciation rule sets',
        admin: 'Users, configuration, and integrations',
        'admin-users': 'User accounts, teams, and role permissions',
        'admin-security': 'Password rules, account lockout, and complexity policy',
        'admin-audit': 'Complete audit trail of all user activity, with export',
        'admin-logs': 'Application/event log — HTTP, SMTP, webhook, and Teams activity',
        'admin-config': 'Application settings, maintenance defaults, and tax rule sets',
        'admin-integrations': 'Email, webhooks, ServiceNow, and Workday connectors'
      }[this.activeView];
    },
    currentTitle() {
      return {
        dashboard: 'Dashboard',
        assets: 'Asset register',
        scan: 'Scan & update',
        alerts: 'Alerts',
        pm: 'Preventative maintenance',
        parts: 'Parts inventory',
        assignments: 'Assignments',
        contacts: 'Contacts',
        reports: 'Reports',
        books: 'Books',
        'gl-journals': 'GL and Journals',
        close: 'Period Close',
        'section-179': 'Section 179',
        templates: 'Templates',
        'tax-rules': 'Tax rules',
        admin: 'Configuration',
        'admin-users': 'Users & Teams',
        'admin-security': 'Password & Security',
        'admin-audit': 'Activity & Audit Trail',
        'admin-logs': 'Application Logs',
        'admin-config': 'Application Configuration',
        'admin-integrations': 'Integrations'
      }[this.activeView];
    },
    entityOptions() {
      return this.entities.map((entity) => ({ title: entity.name, value: entity.id }));
    },
    activeCompany() {
      return this.entities.find((entity) => entity.id === this.activeCompanyId) || null;
    },
    activeBookItems() {
      return this.bookCodes.length ? this.bookCodes : this.bookItems;
    },
    labelAssets() {
      if (this.labelAssetsOverride) return this.labelAssetsOverride;
      if (this.selectedAssetIds.length) return this.assets.filter((asset) => this.selectedAssetIds.includes(asset.id));
      return this.assets;
    },
    activeTheme() {
      return this.profilePreferences.theme || 'depreCoreDark';
    },
    fluentLuminance() {
      return darkThemes.includes(this.activeTheme) ? '0.12' : '1';
    },
    methodItems() {
      const methods = this.taxRules[0]?.rules_json?.methods || [];
      const fallback = [
        { code: 'straight_line', family: 'GAAP', label: 'Straight-line' },
        { code: 'sum_of_years_digits', family: 'GAAP', label: 'Sum-of-years-digits' },
        { code: 'declining_balance_200', family: 'GAAP', label: '200% declining balance' },
        { code: 'manual', family: 'Manual', label: 'Manual depreciation entry' }
      ];
      return (methods.length ? methods : fallback).map((method) => ({
        title: `${method.family} · ${method.label}`,
        value: method.code
      }));
    },
    selectedTemplate() {
      return this.templates.find((template) => template.id === this.selectedTemplateId) || null;
    },
    selectedTemplateFields() {
      return normalizeCustomFields(this.selectedTemplate?.custom_fields || []);
    },
    templateOptions() {
      return this.templates.map((template) => ({ title: template.name, value: template.id }));
    },
    visibleNavItems() {
      // Each screen maps to the capability/capabilities (any-of) that reveal it; absent = always.
      const navCaps = {
        assets: ['assets.view'],
        scan: ['assets.edit'],
        assignments: ['assets.view'],
        templates: ['assets.view'],
        alerts: ['alerts.manage'],
        pm: ['pm.view'],
        parts: ['parts.manage'],
        contacts: ['contacts.manage'],
        reports: ['finance.view'],
        books: ['finance.manage'],
        'gl-journals': ['finance.view', 'finance.manage'],
        close: ['finance.close', 'finance.manage'],
        'section-179': ['finance.view', 'finance.manage'],
        'tax-rules': ['finance.manage'],
        'admin-users': ['admin.users'],
        'admin-security': ['admin.security'],
        'admin-audit': ['admin.audit'],
        'admin-logs': ['admin.logs'],
        'admin-config': ['admin.settings', 'config.manage'],
        'admin-integrations': ['admin.integrations']
      };
      const visible = (key) => this.canAny(navCaps[key]);
      return this.navItems
        .map((item) => {
          if (!item.children) return visible(item.key) ? item : null;
          const children = item.children.filter((child) => visible(child.key));
          // Screen-parents (assets, pm) show if the parent or any child is visible;
          // group-only parents (financial, admin) show only when a child is visible.
          const show = item.group ? children.length > 0 : (visible(item.key) || children.length > 0);
          return show ? { ...item, children } : null;
        })
        .filter(Boolean);
    }
  },
  watch: {
    activeView(view) {
      if (view.startsWith('admin')) this.loadAdmin();
      if (view === 'assignments') this.loadAssignments();
    },
    visibleNavItems(items) {
      const keys = items.flatMap((item) => [item.key, ...(item.children || []).map((child) => child.key)]);
      if (!keys.includes(this.activeView)) {
        this.activeView = items[0]?.key || 'dashboard';
      }
    },
    labelDialog(open) {
      if (!open) this.labelAssetsOverride = null;
    },
    profilePreferences: {
      handler() {
        this.applyAppearance();
      },
      deep: true
    },
    token() {
      // The v-app (and its theme root) only exists once signed in — re-apply then.
      this.$nextTick(() => this.applyAppearance());
    }
  },
  mounted() {
    this.applyAppearance();
    window.addEventListener('deprecore:unauthorized', this.handleUnauthorized);
    if (this.token) this.loadInitial();
  },
  beforeUnmount() {
    window.removeEventListener('deprecore:unauthorized', this.handleUnauthorized);
  },
  methods: {
    currency,
    shortDate,
    // Capability checks driven by the signed-in user's role capabilities ('*' = all).
    can(capability) {
      return this.userCapabilities.includes('*') || this.userCapabilities.includes(capability);
    },
    canAny(capabilities) {
      if (!capabilities || !capabilities.length) return true;
      return capabilities.some((cap) => this.can(cap));
    },
    setCapabilities(capabilities) {
      this.userCapabilities = Array.isArray(capabilities) ? capabilities : [];
      localStorage.setItem('deprecore.capabilities', JSON.stringify(this.userCapabilities));
    },
    // Apply accessibility/appearance preferences to the live DOM: base text size (scales
    // the whole UI via the root font-size), a custom accent colour, and a high-contrast boost.
    applyAppearance() {
      document.documentElement.style.fontSize = fontScaleMap[this.profilePreferences.fontScale] || fontScaleMap.normal;
      document.body.classList.toggle('ma-contrast-boost', Boolean(this.profilePreferences.contrast));
      this.$nextTick(() => {
        const root = document.querySelector('.v-application');
        if (!root) return;
        const triplet = this.hexToTriplet(this.profilePreferences.accent);
        if (triplet) {
          // Inline styles win over Vuetify's generated per-theme variables.
          root.style.setProperty('--v-theme-primary', triplet);
          root.style.setProperty('--v-theme-on-primary', this.readableOn(triplet));
        } else {
          root.style.removeProperty('--v-theme-primary');
          root.style.removeProperty('--v-theme-on-primary');
        }
      });
    },
    hexToTriplet(hex) {
      const match = /^#?([0-9a-f]{6})$/i.exec(String(hex || '').trim());
      if (!match) return null;
      const int = parseInt(match[1], 16);
      return `${(int >> 16) & 255},${(int >> 8) & 255},${int & 255}`;
    },
    // Pick black or white text for legibility on the chosen accent (WCAG luminance).
    readableOn(triplet) {
      const [r, g, b] = triplet.split(',').map(Number);
      const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
      return lum > 0.6 ? '0,0,0' : '255,255,255';
    },
    persistAppearanceLocal() {
      localStorage.setItem('deprecore.theme', this.activeTheme);
      localStorage.setItem('deprecore.fontScale', this.profilePreferences.fontScale || 'normal');
      localStorage.setItem('deprecore.accent', this.profilePreferences.accent || '');
      localStorage.setItem('deprecore.contrast', String(Boolean(this.profilePreferences.contrast)));
    },
    async api(path, options = {}) {
      return apiRequest(path, this.token, options);
    },
    async addEmployee(employee) {
      await this.api('/api/employees', { method: 'POST', body: JSON.stringify(employee) });
      await this.loadAssignments();
    },
    applyTemplate(id) {
      this.selectedTemplateId = id || null;
      const template = this.templates.find((item) => item.id === id);
      if (!template) {
        this.assetForm = { ...this.assetForm, template_id: null };
        return;
      }
      const customFields = { ...(this.assetForm.custom_fields || {}) };
      for (const field of normalizeCustomFields(template.custom_fields)) {
        if (customFields[field.key] === undefined) customFields[field.key] = defaultValueForField(field);
      }
      const merged = { ...this.assetForm, ...template.defaults, custom_fields: customFields, template_id: template.id };
      // For a brand-new asset, realign the book defaults to the template's method/life.
      if (!merged.id) merged.books = makeDefaultBooks(merged);
      this.assetForm = merged;
    },
    async assignAsset(form) {
      this.assignmentSaving = true;
      try {
        await this.api('/api/assignments', { method: 'POST', body: JSON.stringify(form) });
        await Promise.all([this.loadAssignments(), this.loadAssets(), this.loadDashboard()]);
      } finally {
        this.assignmentSaving = false;
      }
    },
    async createTeam(team) {
      this.teamSaving = true;
      this.teamNotice = '';
      try {
        await this.api('/api/teams', { method: 'POST', body: JSON.stringify(team) });
        await this.loadAdmin();
      } catch (error) {
        this.teamNotice = error.message;
      } finally {
        this.teamSaving = false;
      }
    },
    async updateTeam(team) {
      this.teamSaving = true;
      this.teamNotice = '';
      try {
        await this.api(`/api/teams/${team.id}`, { method: 'PUT', body: JSON.stringify(team) });
        await this.loadAdmin();
      } catch (error) {
        this.teamNotice = error.message;
      } finally {
        this.teamSaving = false;
      }
    },
    async deleteTeam(id) {
      this.teamNotice = '';
      try {
        await this.api(`/api/teams/${id}`, { method: 'DELETE' });
        await this.loadAdmin();
      } catch (error) {
        this.teamNotice = error.message;
      }
    },
    notify(text, color = 'error') {
      this.snackbar = { show: true, text, color };
    },
    async createUser(user) {
      this.userSaving = true;
      try {
        await this.api('/api/users', { method: 'POST', body: JSON.stringify(user) });
        await this.loadAdmin();
        this.notify('User created.', 'success');
      } catch (error) {
        this.notify(error.message);
      } finally {
        this.userSaving = false;
      }
    },
    async unlockUser(account) {
      try {
        await this.api(`/api/users/${account.id}/unlock`, { method: 'POST' });
        await this.loadAdmin();
        this.notify(`${account.name} unlocked.`, 'success');
      } catch (error) {
        this.notify(error.message);
      }
    },
    async editAsset(asset) {
      this.drawerError = '';
      this.disposalPreview = null;
      this.leaseSchedule = null;
      this.selectedLeaseId = null;
      this.loadPmContext();
      this.loadReferences(); this.loadDepreciationZones();
      this.loadContactOptions();
      await this.loadBookCodes();
      await this.refreshActiveAsset(asset.id, true);
      this.assetDrawer = true;
    },
    async refreshActiveAsset(id, syncForm = false) {
      const data = await (await this.api(`/api/assets/${id}`)).json();
      const asset = data.asset;
      this.activeFiles = asset.files || [];
      this.activeWarranties = asset.warranties || [];
      this.activeTasks = asset.tasks || [];
      this.activeNotes = asset.notes_log || [];
      this.activePhotos = asset.photos || [];
      this.activeDisposals = asset.disposals || [];
      this.activeAdjustments = asset.adjustments || [];
      this.activeLeases = asset.leases || [];
      this.activePm = asset.pm || [];
      if (syncForm) {
        this.assetForm = {
          ...blankAsset(),
          ...asset,
          custom_fields: asset.custom_fields || {},
          books: mergeBooks(asset.books || [], this.bookCodes, asset)
        };
        this.selectedTemplateId = asset.template_id || null;
      }
      return asset;
    },
    clearActiveCollections() {
      this.activeFiles = [];
      this.activeWarranties = [];
      this.activeTasks = [];
      this.activeNotes = [];
      this.activePhotos = [];
      this.activeDisposals = [];
      this.activeAdjustments = [];
      this.activeLeases = [];
      this.activePm = [];
      this.disposalPreview = null;
      this.leaseSchedule = null;
      this.selectedLeaseId = null;
    },
    async previewDisposal(form) {
      const data = await (await this.api(`/api/assets/${this.assetForm.id}/disposal/preview`, {
        method: 'POST',
        body: JSON.stringify(form)
      })).json();
      this.disposalPreview = data.preview;
    },
    async recordDisposal(form) {
      await this.api(`/api/assets/${this.assetForm.id}/disposals`, { method: 'POST', body: JSON.stringify(form) });
      this.disposalPreview = null;
      await this.refreshActiveAsset(this.assetForm.id, true);
      await this.loadAssets();
      await this.loadDashboard();
    },
    async reverseDisposal(disposalId) {
      await this.api(`/api/assets/${this.assetForm.id}/disposals/${disposalId}`, { method: 'DELETE' });
      await this.refreshActiveAsset(this.assetForm.id, true);
      await this.loadAssets();
      await this.loadDashboard();
    },
    async addAdjustment(form) {
      await this.api(`/api/assets/${this.assetForm.id}/adjustments`, { method: 'POST', body: JSON.stringify(form) });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async deleteAdjustment(id) {
      await this.api(`/api/assets/${this.assetForm.id}/adjustments/${id}`, { method: 'DELETE' });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async saveLease(form) {
      await this.api('/api/leases', { method: 'POST', body: JSON.stringify({ ...form, asset_id: this.assetForm.id }) });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async deleteLease(id) {
      await this.api(`/api/leases/${id}`, { method: 'DELETE' });
      if (this.selectedLeaseId === id) {
        this.leaseSchedule = null;
        this.selectedLeaseId = null;
      }
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async loadLeaseSchedule(leaseId) {
      const data = await (await this.api(`/api/leases/${leaseId}/schedule`)).json();
      this.selectedLeaseId = leaseId;
      this.leaseSchedule = data.schedule;
    },
    async uploadAssetFile(file) {
      const formData = new FormData();
      formData.append('file', file);
      await this.api(`/api/assets/${this.assetForm.id}/files`, { method: 'POST', body: formData });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async downloadAssetFile(file) {
      const blob = await (await this.api(`/api/assets/${this.assetForm.id}/files/${file.id}`)).blob();
      saveBlob(blob, file.file_name);
    },
    async deleteAssetFile(fileId) {
      await this.api(`/api/assets/${this.assetForm.id}/files/${fileId}`, { method: 'DELETE' });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async uploadAssetPhoto(photo) {
      await this.api(`/api/assets/${this.assetForm.id}/photos`, { method: 'POST', body: JSON.stringify(photo) });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async updateAssetPhoto({ id, caption }) {
      await this.api(`/api/assets/${this.assetForm.id}/photos/${id}`, { method: 'PUT', body: JSON.stringify({ caption }) });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async deleteAssetPhoto(id) {
      await this.api(`/api/assets/${this.assetForm.id}/photos/${id}`, { method: 'DELETE' });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async addWarranty(warranty) {
      await this.api(`/api/assets/${this.assetForm.id}/warranties`, { method: 'POST', body: JSON.stringify(warranty) });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async deleteWarranty(id) {
      await this.api(`/api/assets/${this.assetForm.id}/warranties/${id}`, { method: 'DELETE' });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async addTask(task) {
      await this.api(`/api/assets/${this.assetForm.id}/tasks`, { method: 'POST', body: JSON.stringify(task) });
      await this.refreshActiveAsset(this.assetForm.id);
      await this.loadDashboard();
    },
    async toggleTask(task) {
      const status = task.status === 'complete' ? 'open' : 'complete';
      await this.api(`/api/assets/${this.assetForm.id}/tasks/${task.id}`, { method: 'PUT', body: JSON.stringify({ status }) });
      await this.refreshActiveAsset(this.assetForm.id);
      await this.loadDashboard();
    },
    async deleteTask(id) {
      await this.api(`/api/assets/${this.assetForm.id}/tasks/${id}`, { method: 'DELETE' });
      await this.refreshActiveAsset(this.assetForm.id);
      await this.loadDashboard();
    },
    async addNote(body) {
      await this.api(`/api/assets/${this.assetForm.id}/notes`, { method: 'POST', body: JSON.stringify({ body }) });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async deleteNote(id) {
      await this.api(`/api/assets/${this.assetForm.id}/notes/${id}`, { method: 'DELETE' });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async attachPm(form) {
      await this.api(`/api/assets/${this.assetForm.id}/pm`, { method: 'POST', body: JSON.stringify(form) });
      await this.refreshActiveAsset(this.assetForm.id);
      await this.loadDashboard();
    },
    async detachPm(apId) {
      await this.api(`/api/assets/${this.assetForm.id}/pm/${apId}`, { method: 'DELETE' });
      await this.refreshActiveAsset(this.assetForm.id);
    },
    async completePm(payload) {
      const { apId, ...body } = payload;
      await this.api(`/api/assets/${this.assetForm.id}/pm/${apId}/complete`, {
        method: 'POST',
        body: JSON.stringify(body)
      });
      await this.refreshActiveAsset(this.assetForm.id);
      await this.loadDashboard();
    },
    async logReading(payload) {
      const { apId, ...body } = payload;
      try {
        await this.api(`/api/assets/${this.assetForm.id}/pm/${apId}/readings`, {
          method: 'POST',
          body: JSON.stringify(body)
        });
        await this.refreshActiveAsset(this.assetForm.id);
        this.notify('Reading logged.', 'success');
      } catch (error) {
        this.notify(error.message);
      }
    },
    viewCompletion(id) {
      this.completionId = id;
      this.completionDialog = true;
    },
    async loadBookCodes() {
      try {
        const data = await (await this.api('/api/books')).json();
        this.bookCodes = (data.books || [])
          .filter((book) => book.enabled && book.book_type !== 'maintenance')
          .map((book) => book.code);
      } catch {
        // keep the constant fallback
      }
    },
    async loadPmContext() {
      const [plans, settings] = await Promise.all([
        this.api('/api/pm-plans').then((r) => r.json()).catch(() => ({ plans: [] })),
        this.api('/api/pm-settings').then((r) => r.json()).catch(() => ({ settings: {} }))
      ]);
      this.pmPlans = plans.plans || [];
      if (settings.settings) {
        this.pmDefaults = {
          value: settings.settings.pm_default_frequency_value || 3,
          unit: settings.settings.pm_default_frequency_unit || 'months'
        };
      }
    },
    async exportAssets(format) {
      const blob = await (await this.api(`/api/export/assets?format=${format}`)).blob();
      saveBlob(blob, `deprecore-assets.${format}`);
    },
    async loadReferences() {
      const data = await (await this.api('/api/references')).json();
      this.references = data.references;
    },
    async loadDepreciationZones() {
      try {
        const data = await (await this.api('/api/depreciation-zones')).json();
        this.depreciationZones = data.zones || [];
      } catch {
        this.depreciationZones = [];
      }
    },
    async loadContactOptions() {
      try {
        const data = await (await this.api('/api/contacts')).json();
        const contacts = data.contacts || [];
        this.workLocations = contacts.filter((c) => c.type === 'work_location').map((c) => c.display_name).filter(Boolean);
        this.vendorContacts = contacts
          .filter((c) => ['vendor', 'service_provider', 'contractor'].includes(c.type))
          .map((c) => c.display_name)
          .filter(Boolean);
      } catch {
        this.workLocations = [];
        this.vendorContacts = [];
      }
    },
    filterAssets(filters) {
      this.assetFilters = filters;
      this.loadAssets();
    },
    async importAssets(event) {
      const [file] = event.target.files || [];
      if (!file) return;
      const formData = new FormData();
      formData.append('file', file);
      await this.api('/api/import/assets', { method: 'POST', body: formData });
      event.target.value = '';
      await this.loadAssets();
      await this.loadDashboard();
    },
    async loadAdmin() {
      const [settings, rules, workday, users, teams, accessControl] = await Promise.all([
        this.api('/api/settings').then((r) => r.json()).catch(() => ({ settings: {} })),
        this.api('/api/tax-rules').then((r) => r.json()),
        this.api('/api/workday/connection').then((r) => r.json()).catch(() => ({ connection: {} })),
        this.api('/api/users').then((r) => r.json()).catch(() => ({ users: [] })),
        this.api('/api/teams').then((r) => r.json()).catch(() => ({ teams: [] })),
        this.api('/api/access-control').then((r) => r.json()).catch(() => ({ roles: [], capabilities: [] }))
      ]);
      this.settings = settings.settings;
      this.taxRules = rules.ruleSets;
      this.workdayConnection = workday.connection || {};
      this.users = users.users || [];
      this.teams = teams.teams || [];
      this.roles = accessControl.roles || [];
      this.capabilityCatalog = accessControl.capabilities || [];
    },
    async createRole(role) {
      await this.api('/api/roles', { method: 'POST', body: JSON.stringify(role) });
      await this.loadAdmin();
    },
    async updateRole(role) {
      await this.api(`/api/roles/${role.key}`, { method: 'PUT', body: JSON.stringify(role) });
      await this.loadAdmin();
      await this.loadProfile(); // capabilities may have changed for the current user's role
    },
    async deleteRole(key) {
      await this.api(`/api/roles/${key}`, { method: 'DELETE' });
      await this.loadAdmin();
    },
    async loadAssets() {
      const params = new URLSearchParams();
      if (this.assetFilters.search) params.set('search', this.assetFilters.search);
      if (this.assetFilters.status) params.set('status', this.assetFilters.status);
      const data = await (await this.api(`/api/assets?${params.toString()}`)).json();
      this.assets = data.assets;
      this.selectedAssetIds = this.selectedAssetIds.filter((id) => this.assets.some((asset) => asset.id === id));
    },
    // The dashboard is now a self-loading widget designer (each widget fetches its own data and the view
    // remounts on company switch), so App no longer pre-fetches dashboard data. Retained as a no-op since
    // many mutation handlers still call it to "refresh the dashboard".
    async loadDashboard() {},
    async loadInitial() {
      // Resolve the active company first so every other request is scoped to it.
      await this.loadCompanies();
      await Promise.all([
        this.loadProfile(),
        this.loadDashboard(),
        this.loadAssets(),
        this.loadAssignments(),
        this.api('/api/references').then((r) => r.json()).then((data) => { this.references = data.references; }),
        this.api('/api/templates').then((r) => r.json()).then((data) => { this.templates = data.templates; }),
        this.api('/api/tax-rules').then((r) => r.json()).then((data) => { this.taxRules = data.ruleSets; }),
        this.loadPmContext(),
        this.loadBookCodes(),
        this.loadContactOptions(),
        this.loadDepreciationZones()
      ]);
    },
    // Load the active companies and pick the active one (stored choice if still valid, else the first).
    async loadCompanies() {
      const data = await (await this.api('/api/entities')).json();
      this.entities = data.entities || [];
      const stored = Number(localStorage.getItem('deprecore.activeCompany'));
      const valid = this.entities.find((e) => e.id === stored);
      this.activeCompanyId = valid ? valid.id : (this.entities[0]?.id || null);
      localStorage.setItem('deprecore.activeCompany', String(this.activeCompanyId || ''));
      setActiveCompany(this.activeCompanyId);
    },
    // Switch the active company: persist it, update the request header, and reload all scoped data.
    async changeCompany(id) {
      if (!id || id === this.activeCompanyId) return;
      this.activeCompanyId = id;
      localStorage.setItem('deprecore.activeCompany', String(id));
      setActiveCompany(id);
      await this.loadInitial();
    },
    async loadAssignments() {
      const [employees, assignments, workday] = await Promise.all([
        this.api('/api/employees').then((r) => r.json()),
        this.api('/api/assignments').then((r) => r.json()),
        this.api('/api/workday/connection').then((r) => r.json()).catch(() => ({ connection: {} }))
      ]);
      this.employees = employees.employees;
      this.assignments = assignments.assignments;
      this.assignmentSummary = assignments.summary;
      this.workdayConnection = workday.connection || {};
    },
    async loadProfile() {
      const profile = await (await this.api('/api/profile')).json();
      this.user = profile.user;
      this.profilePreferences = { theme: 'depreCoreDark', fontScale: 'normal', accent: '', contrast: false, ...(profile.preferences || {}) };
      this.setCapabilities(profile.capabilities);
      localStorage.setItem('deprecore.user', JSON.stringify(profile.user));
      this.persistAppearanceLocal();
    },
    async login(form) {
      this.loading = true;
      this.error = '';
      try {
        const data = await loginRequest(form);
        this.loginForm = { ...form };
        this.token = data.token;
        this.user = data.user;
        this.setCapabilities(data.capabilities);
        localStorage.setItem('deprecore.token', data.token);
        localStorage.setItem('deprecore.user', JSON.stringify(data.user));
        // A forced/expired password gates the app behind the change-password step (token is held
        // so the change endpoint is callable, but app data isn't loaded until the change succeeds).
        if (data.mustChangePassword) {
          this.forcePasswordChange = true;
        } else {
          await this.loadInitial();
        }
      } catch (error) {
        this.error = error.message;
      } finally {
        this.loading = false;
      }
    },
    async completeForcedChange() {
      this.forcePasswordChange = false;
      this.error = '';
      this.loginForm = { ...this.loginForm, password: '' };
      await this.loadInitial();
    },
    logout() {
      this.token = '';
      this.user = null;
      this.forcePasswordChange = false;
      this.userCapabilities = [];
      localStorage.removeItem('deprecore.token');
      localStorage.removeItem('deprecore.user');
      localStorage.removeItem('deprecore.capabilities');
    },
    // Triggered when any API call reports an invalid/expired token: return to the login screen.
    handleUnauthorized() {
      if (!this.token) return; // already signed out
      this.logout();
      this.error = 'Your session has expired. Please sign in again.';
    },
    async massUpdate(changesInput) {
      const changes = Object.fromEntries(Object.entries(changesInput).filter(([, value]) => value !== '' && value !== null));
      await this.api('/api/assets/mass-update', { method: 'POST', body: JSON.stringify({ ids: this.selectedAssetIds, changes }) });
      this.massDialog = false;
      this.massChanges = { status: null, location: '', department: '' };
      await this.loadAssets();
      await this.loadDashboard();
    },
    toggleNav() {
      this.navCollapsed = !this.navCollapsed;
      localStorage.setItem('deprecore.navCollapsed', String(this.navCollapsed));
    },
    // Group-only items (no own screen) just expand; real screens navigate.
    selectNav(item) {
      if (!item.group) this.activeView = item.key;
    },
    isNavActive(item) {
      if (this.activeView === item.key) return true;
      return (item.children || []).some((child) => child.key === this.activeView);
    },
    async reloadTaxRules() {
      const data = await (await this.api('/api/tax-rules')).json();
      this.taxRules = data.ruleSets;
    },
    printLabels() {
      this.labelAssetsOverride = null;
      this.labelDialog = true;
    },
    printSingleLabel(asset) {
      this.labelAssetsOverride = [asset];
      this.labelDialog = true;
    },
    async onScanAssetUpdated() {
      await Promise.all([this.loadAssets(), this.loadDashboard()]);
    },
    async newAsset() {
      await this.loadBookCodes();
      this.assetForm = blankAsset();
      this.assetForm.entity_id = this.activeCompanyId || this.entities[0]?.id || null;
      this.assetForm.books = makeDefaultBooks(this.assetForm, this.bookCodes);
      this.selectedTemplateId = null;
      this.drawerError = '';
      this.clearActiveCollections();
      this.loadPmContext();
      this.loadReferences(); this.loadDepreciationZones();
      this.loadContactOptions();
      this.assetDrawer = true;
    },
    async saveAsset(assetInput) {
      this.saving = true;
      this.drawerError = '';
      try {
        const books = (assetInput.books && assetInput.books.length ? assetInput.books : makeDefaultBooks(assetInput))
          .map((book) => ({
            ...book,
            cost: book.cost === '' || book.cost === null || book.cost === undefined ? null : Number(book.cost)
          }));
        const payload = { ...assetInput, books };
        const method = assetInput.id ? 'PUT' : 'POST';
        const url = assetInput.id ? `/api/assets/${assetInput.id}` : '/api/assets';
        const saved = await (await this.api(url, { method, body: JSON.stringify(payload) })).json();
        await this.loadAssets();
        await this.loadDashboard();
        if (!assetInput.id && saved.asset) {
          // Keep the drawer open on the freshly created asset so files, warranties,
          // tasks, notes, disposals, and leases can be attached without reopening.
          await this.refreshActiveAsset(saved.asset.id, true);
        } else {
          this.assetDrawer = false;
        }
      } catch (error) {
        this.drawerError = error.message;
      } finally {
        this.saving = false;
      }
    },
    async saveSettings(settings) {
      await this.api('/api/settings', { method: 'PUT', body: JSON.stringify({ settings }) });
      await this.loadAdmin();
    },
    async releaseAssignment(assignment) {
      await this.api(`/api/assignments/${assignment.id}/release`, {
        method: 'PUT',
        body: JSON.stringify({ release_reason: 'Released from UI' })
      });
      await Promise.all([this.loadAssignments(), this.loadAssets()]);
    },
    async saveProfilePreferences(preferences, options = { persist: true }) {
      this.profilePreferences = { ...this.profilePreferences, ...(preferences || {}) };
      this.persistAppearanceLocal();
      if (!options.persist) return;

      this.preferenceSaving = true;
      try {
        const profile = await (await this.api('/api/profile', {
          method: 'PUT',
          body: JSON.stringify({ preferences: this.profilePreferences })
        })).json();
        this.user = profile.user;
        this.profilePreferences = { theme: 'depreCoreDark', fontScale: 'normal', accent: '', contrast: false, ...(profile.preferences || {}) };
        localStorage.setItem('deprecore.user', JSON.stringify(profile.user));
        this.persistAppearanceLocal();
      } finally {
        this.preferenceSaving = false;
      }
    },
    async saveWorkdayConnection(connection) {
      this.workdaySaving = true;
      try {
        const body = { ...connection };
        if (!body.client_secret) delete body.client_secret;
        await this.api('/api/workday/connection', { method: 'PUT', body: JSON.stringify(body) });
        await this.loadAssignments();
      } finally {
        this.workdaySaving = false;
      }
    },
    async saveTemplate(form) {
      const payload = {
        ...form,
        custom_fields: normalizeCustomFields(form.custom_fields)
      };
      const method = form.id ? 'PUT' : 'POST';
      const url = form.id ? `/api/templates/${form.id}` : '/api/templates';
      await this.api(url, { method, body: JSON.stringify(payload) });
      await this.reloadTemplates();
      this.resetTemplateForm();
    },
    async deleteTemplate(id) {
      await this.api(`/api/templates/${id}`, { method: 'DELETE' });
      await this.reloadTemplates();
      if (this.templateForm.id === id) this.resetTemplateForm();
    },
    async reloadTemplates() {
      const data = await (await this.api('/api/templates')).json();
      this.templates = data.templates;
    },
    resetTemplateForm() {
      this.templateForm = {
        id: null,
        name: '',
        description: '',
        defaults: { category: 'Computer Equipment', useful_life_months: 60, depreciation_method: 'straight_line' },
        custom_fields: []
      };
    },
    async syncWorkday() {
      this.workdaySyncing = true;
      try {
        await this.api('/api/workday/sync-workers', { method: 'POST', body: JSON.stringify({}) });
        await this.loadAssignments();
      } finally {
        this.workdaySyncing = false;
      }
    },
    async updateUser(user) {
      this.userSaving = true;
      try {
        const body = { ...user };
        if (!body.password) delete body.password;
        await this.api(`/api/users/${user.id}`, { method: 'PUT', body: JSON.stringify(body) });
        await this.loadAdmin();
        this.notify('User updated.', 'success');
      } catch (error) {
        this.notify(error.message);
      } finally {
        this.userSaving = false;
      }
    },
    toggleAll(value) {
      this.selectedAssetIds = value ? this.assets.map((asset) => asset.id) : [];
    },
    toggleAsset(id) {
      this.selectedAssetIds = this.selectedAssetIds.includes(id)
        ? this.selectedAssetIds.filter((assetId) => assetId !== id)
        : [...this.selectedAssetIds, id];
    }
  }
};
</script>
