Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { getDb } from "@/dataAccess/getDb";
import { Preferences } from "@capacitor/preferences";
import type { OffersRecord, ProblemsRecord, ProblemTagsRecord } from "@/pocketbase-types";
import type { JobContentHierarchy } from "@/framework/jobContent";
import { storeCurrency, type CurrencyPrefs } from "@/framework/currencies";
import { save as saveLog } from "@/framework/logs";
import { getApi } from "@/dataAccess/getApi";
import { get as getPref, set as setPref } from "@/framework/prefs";
import { countJobs } from "@/tnfr/logs";

// Re-export types from session store for compatibility
export interface MenuModifications {
  menuTitle?: string;
  tierModifications?: {
    [tierId: string]: {
      offerTitle?: string;
      offerDetails?: string;
      hidden?: boolean;
    };
  };
}

export interface JobRecord {
  id: string;
  title: string;
  problem: ProblemsRecord;
  menus: any[];
  baseHours: number;
  extraTime: number;
  extraMaterial: number;
  selectedOffer: OffersRecord;
  selectedPrice: string;
  selectedTierName?: string;
  selectedTierTitle?: string;
  selectedTierContent?: string[];
  notes: string[];
  completedClipboards?: number[];
  menuModifications?: MenuModifications;
  menuData?: any;
  checklistIssues?: string[];
  hoursConfirmed?: boolean;
  edited?: boolean;
}

export interface SessionThemeColors {
  primary: string;
  secondary: string;
  tertiary: string;
  success: string;
  warning: string;
  danger: string;
  light: string;
  medium: string;
  dark: string;
  background: string;
  text: string;
}

export interface MenuThemeColors {
  platinum: string;
  gold: string;
  silver: string;
  bronze: string;
  bandaid: string;
}

export type CustomerProgress = (string | boolean)[];

export interface InvoiceLineItem {
  jobId: string;
  jobTitle: string;
  offerId: string;
  offerRefId: string;
  menuName: string;
  tierName: string;
  tierTitle: string;
  price: number;
  discountPrice?: number;
  tierContent?: string[];
  offer?: any;
}

export interface Invoice {
  paymentConfirmed: boolean;
  paymentInfo: string;
  lineItems: InvoiceLineItem[];
  customerName: string;
  customerAddress: string;
  customerPhone: string;
  customerEmail: string;
}

export const useTnfrStore = defineStore('tnfr', () => {
  // Database reference
  const db = ref<Awaited<ReturnType<typeof getDb>> | null>(null);

  // ============ Session state ============
  const sessionId = ref(0);
  const startTime = ref("");
  const activeJob = ref("");
  const jobs = ref<JobRecord[]>([]);
  const pendingJob = ref<JobRecord | null>(null);
  const endTime = ref("");
  const applyDiscount = ref(false);
  const showPlan = ref(false);
  const serviceCallsCount = ref(0);
  const menusShownCount = ref(0);
  const menusShownGoal = ref(3);
  const higherTierChosenCount = ref(0);
  const serviceCallsWithMenusShown = ref(0);
  const themeColors = ref<SessionThemeColors>({} as SessionThemeColors);
  const menuThemeColors = ref<MenuThemeColors>({} as MenuThemeColors);
  const problems = ref<ProblemsRecord[]>([]);
  const problemTags = ref<ProblemTagsRecord[]>([]);
  const selectedTags = ref<string[]>([]);
  const appliedSearchQuery = ref("");
  const selectionHistory = ref<Array<{ tags: string[], search: string }>>([]);
  const paymentMethod = ref("");
  const editMode = ref(false);
  const secretMode = ref(false);
  const customerProgress = ref<CustomerProgress>([]);
  const invoice = ref<Invoice>({
    paymentConfirmed: false,
    paymentInfo: '',
    lineItems: [],
    customerName: '',
    customerAddress: '',
    customerPhone: '',
    customerEmail: '',
  });
  const seenMenuJobIds = ref<string[]>([]);
  const speedDial = ref<string[]>([]);

  // ============ Preferences state ============
  const darkMode = ref(false);
  const currency = ref("usd");
  const role = ref("tech");
  const darkModeToggleCount = ref(0);

  // ============ Currency state ============
  const currencies = ref<any[]>([]);
  const exchangeRates = ref<any[]>([]);
  const currencyPrefs = ref<CurrencyPrefs | null>(null);

  // ============ Organization state ============
  const orgIsLoading = ref(false);
  const hourlyFee = ref(5.262);
  const saDiscount = ref(1.5);
  const serviceCallFee = ref(2.0);
  const salesTax = ref(1.08);
  const paymentPlanRate = ref(0.07); // 7% annual rate
  const paymentPlanNumberPayments = ref(12); // 12 monthly payments

  // Calculate monthly payment using amortization formula
  // P = (L * r) / (1 - (1 + r)^(-n))
  function calculateMonthlyPayment(principal: number): number {
    if (principal <= 0) return 0;
    const r = paymentPlanRate.value / paymentPlanNumberPayments.value; // rate per period
    const n = paymentPlanNumberPayments.value;

    if (r === 0) return principal / n; // No interest case

    const payment = (principal * r) / (1 - Math.pow(1 + r, -n));
    return payment;
  }

  // ============ Menu bullet functions ============
  // Get all unique bullets from all tiers in a job's menu
  function getMenuBullets(jobId: string): Array<{ id: string; content: string }> {
    const job = jobs.value.find(j => j.id === jobId);
    if (!job?.menus?.[0]?.tiers) return [];

    const seenIds = new Set<string>();
    const uniqueItems: Array<{ id: string; content: string }> = [];

    for (const tier of job.menus[0].tiers) {
      // Check menuCopy first
      if (tier.menuCopy && Array.isArray(tier.menuCopy)) {
        for (const copy of tier.menuCopy) {
          if (copy.contentItems && Array.isArray(copy.contentItems)) {
            for (const item of copy.contentItems) {
              if (item.id && !seenIds.has(item.id)) {
                seenIds.add(item.id);
                uniqueItems.push({ id: item.id, content: item.content || '' });
              }
            }
          }
        }
      }
      // Fallback to contentItems (excluding title items)
      else if (tier.contentItems && Array.isArray(tier.contentItems)) {
        for (const item of tier.contentItems) {
          if (item.id && !seenIds.has(item.id) && !item.refId?.includes('_title')) {
            seenIds.add(item.id);
            uniqueItems.push({ id: item.id, content: item.content || '' });
          }
        }
      }
    }

    return uniqueItems;
  }

  // Get bullets for a specific tier
  function getTierBullets(jobId: string, tierId: string): Array<{ id: string; content: string }> {
    const job = jobs.value.find(j => j.id === jobId);
    if (!job?.menus?.[0]?.tiers) return [];

    const tier = job.menus[0].tiers.find((t: any) => t.id === tierId);
    if (!tier) return [];

    const items: Array<{ id: string; content: string }> = [];

    if (tier.menuCopy && Array.isArray(tier.menuCopy)) {
      for (const copy of tier.menuCopy) {
        if (copy.contentItems && Array.isArray(copy.contentItems)) {
          for (const item of copy.contentItems) {
            items.push({ id: item.id, content: item.content || '' });
          }
        }
      }
    } else if (tier.contentItems && Array.isArray(tier.contentItems)) {
      for (const item of tier.contentItems) {
        if (!item.refId?.includes('_title')) {
          items.push({ id: item.id, content: item.content || '' });
        }
      }
    }

    return items;
  }

  // AddBullet: Add a new bullet to the menu pool (adds to all tiers by default)
  async function addMenuBullet(jobId: string, content: string): Promise<string | null> {
    const job = jobs.value.find(j => j.id === jobId);
    if (!job?.menus?.[0]?.tiers) return null;

    const newId = `bullet_${Date.now()}`;
    const newItem = { id: newId, content };

    // Add to all tiers' menuCopy
    for (const tier of job.menus[0].tiers) {
      if (!tier.menuCopy || !Array.isArray(tier.menuCopy)) {
        tier.menuCopy = [{ id: `copy_${tier.id}`, contentItems: [] }];
      }
      if (tier.menuCopy.length === 0) {
        tier.menuCopy.push({ id: `copy_${tier.id}`, contentItems: [] });
      }
      tier.menuCopy[0].contentItems.push({ ...newItem });
    }

    await save();
    return newId;
  }

  // EditBullet: Edit a bullet's content, updating it in menu and all tiers
  async function editMenuBullet(jobId: string, bulletId: string, newContent: string): Promise<boolean> {
    const job = jobs.value.find(j => j.id === jobId);
    if (!job?.menus?.[0]?.tiers) return false;

    let updated = false;

    for (const tier of job.menus[0].tiers) {
      if (tier.menuCopy && Array.isArray(tier.menuCopy)) {
        for (const copy of tier.menuCopy) {
          if (copy.contentItems && Array.isArray(copy.contentItems)) {
            const item = copy.contentItems.find((i: any) => i.id === bulletId);
            if (item) {
              item.content = newContent;
              updated = true;
            }
          }
        }
      }
      if (tier.contentItems && Array.isArray(tier.contentItems)) {
        const item = tier.contentItems.find((i: any) => i.id === bulletId);
        if (item) {
          item.content = newContent;
          updated = true;
        }
      }
    }

    if (updated) {
      await save();
    }
    return updated;
  }

  // RemoveBullet: Remove a bullet from menu and all tiers
  async function removeMenuBullet(jobId: string, bulletId: string): Promise<boolean> {
    const job = jobs.value.find(j => j.id === jobId);
    if (!job?.menus?.[0]?.tiers) return false;

    let removed = false;

    for (const tier of job.menus[0].tiers) {
      if (tier.menuCopy && Array.isArray(tier.menuCopy)) {
        for (const copy of tier.menuCopy) {
          if (copy.contentItems && Array.isArray(copy.contentItems)) {
            const idx = copy.contentItems.findIndex((i: any) => i.id === bulletId);
            if (idx !== -1) {
              copy.contentItems.splice(idx, 1);
              removed = true;
            }
          }
        }
      }
      if (tier.contentItems && Array.isArray(tier.contentItems)) {
        const idx = tier.contentItems.findIndex((i: any) => i.id === bulletId);
        if (idx !== -1) {
          tier.contentItems.splice(idx, 1);
          removed = true;
        }
      }
    }

    if (removed) {
      await save();
    }
    return removed;
  }

  // AssignBullet: Assign a bullet from menu to a specific tier
  async function assignMenuBulletToTier(jobId: string, tierId: string, bulletId: string): Promise<boolean> {
    const job = jobs.value.find(j => j.id === jobId);
    if (!job?.menus?.[0]?.tiers) return false;

    // Find the bullet in the menu pool
    const allBullets = getMenuBullets(jobId);
    const bullet = allBullets.find(b => b.id === bulletId);
    if (!bullet) return false;

    // Find the target tier
    const tier = job.menus[0].tiers.find((t: any) => t.id === tierId);
    if (!tier) return false;

    // Check if bullet already exists in tier
    const tierBullets = getTierBullets(jobId, tierId);
    if (tierBullets.some(b => b.id === bulletId)) return true; // Already assigned

    // Add to tier's menuCopy
    if (!tier.menuCopy || !Array.isArray(tier.menuCopy)) {
      tier.menuCopy = [{ id: `copy_${tier.id}`, contentItems: [] }];
    }
    if (tier.menuCopy.length === 0) {
      tier.menuCopy.push({ id: `copy_${tier.id}`, contentItems: [] });
    }
    tier.menuCopy[0].contentItems.push({ id: bullet.id, content: bullet.content });

    await save();
    return true;
  }

  // RemoveTierBullet: Remove a bullet from a specific tier only (keep in menu pool)
  async function removeBulletFromTier(jobId: string, tierId: string, bulletId: string): Promise<boolean> {
    const job = jobs.value.find(j => j.id === jobId);
    if (!job?.menus?.[0]?.tiers) return false;

    const tier = job.menus[0].tiers.find((t: any) => t.id === tierId);
    if (!tier) return false;

    let removed = false;

    if (tier.menuCopy && Array.isArray(tier.menuCopy)) {
      for (const copy of tier.menuCopy) {
        if (copy.contentItems && Array.isArray(copy.contentItems)) {
          const idx = copy.contentItems.findIndex((i: any) => i.id === bulletId);
          if (idx !== -1) {
            copy.contentItems.splice(idx, 1);
            removed = true;
          }
        }
      }
    }
    if (tier.contentItems && Array.isArray(tier.contentItems)) {
      const idx = tier.contentItems.findIndex((i: any) => i.id === bulletId);
      if (idx !== -1) {
        tier.contentItems.splice(idx, 1);
        removed = true;
      }
    }

    if (removed) {
      await save();
    }
    return removed;
  }

  // ============ Default jobs state ============
  const defaultJob1 = ref<JobContentHierarchy | null>(null);

  // ============ Database initialization ============
  async function initDb() {
    if (!db.value) {
      db.value = await getDb();
    }
  }

  // ============ Session actions ============
  async function load() {
    await initDb();
    if (db.value) {
      const current = (await db.value.sessions.getCurrent()) ?? [];
      if (current[0]) {
        const loadedState = JSON.parse(current[0].state);
        // Load session state
        sessionId.value = current[0].id;
        startTime.value = loadedState.startTime || "";
        activeJob.value = loadedState.activeJob || "";
        jobs.value = loadedState.jobs || [];
        pendingJob.value = loadedState.pendingJob || null;
        endTime.value = loadedState.endTime || "";
        applyDiscount.value = loadedState.applyDiscount || false;
        showPlan.value = loadedState.showPlan || false;
        serviceCallsCount.value = loadedState.serviceCallsCount || 0;
        menusShownCount.value = loadedState.menusShownCount || 0;
        menusShownGoal.value = loadedState.menusShownGoal || 3;
        higherTierChosenCount.value = loadedState.higherTierChosenCount || 0;
        serviceCallsWithMenusShown.value = loadedState.serviceCallsWithMenusShown || 0;
        themeColors.value = loadedState.themeColors || {};
        menuThemeColors.value = loadedState.menuThemeColors || {};
        problems.value = loadedState.problems || [];
        problemTags.value = loadedState.problemTags || [];
        selectedTags.value = loadedState.selectedTags || [];
        appliedSearchQuery.value = loadedState.appliedSearchQuery || "";
        selectionHistory.value = loadedState.selectionHistory || [];
        paymentMethod.value = loadedState.paymentMethod || "";
        editMode.value = loadedState.editMode || false;
        secretMode.value = loadedState.secretMode || false;
        customerProgress.value = loadedState.customerProgress || [];
        invoice.value = loadedState.invoice || { paymentConfirmed: false, paymentInfo: '', lineItems: [], customerName: '', customerAddress: '', customerPhone: '', customerEmail: '' };
        seenMenuJobIds.value = loadedState.seenMenuJobIds || [];
        speedDial.value = loadedState.speedDial || [];
      }
    }
  }

  async function save() {
    await initDb();
    if (db.value) {
      console.log('[TnfrStore] save() called, sessionId:', sessionId.value);
      console.log('[TnfrStore] jobs hoursConfirmed values:', jobs.value.map(j => ({ id: j.id, hoursConfirmed: j.hoursConfirmed })));
      const stateCopy = {
        startTime: startTime.value,
        activeJob: activeJob.value,
        jobs: jobs.value,
        pendingJob: pendingJob.value,
        endTime: endTime.value,
        applyDiscount: applyDiscount.value,
        showPlan: showPlan.value,
        serviceCallsCount: serviceCallsCount.value,
        menusShownCount: menusShownCount.value,
        menusShownGoal: menusShownGoal.value,
        higherTierChosenCount: higherTierChosenCount.value,
        serviceCallsWithMenusShown: serviceCallsWithMenusShown.value,
        themeColors: themeColors.value,
        menuThemeColors: menuThemeColors.value,
        problems: problems.value,
        problemTags: problemTags.value,
        selectedTags: selectedTags.value,
        appliedSearchQuery: appliedSearchQuery.value,
        selectionHistory: selectionHistory.value,
        paymentMethod: paymentMethod.value,
        editMode: editMode.value,
        secretMode: secretMode.value,
        customerProgress: customerProgress.value,
        invoice: invoice.value,
        seenMenuJobIds: seenMenuJobIds.value,
        speedDial: speedDial.value,
      };
      await db.value.sessions.save(JSON.stringify(stateCopy), sessionId.value);
      console.log('[TnfrStore] saved to db');
      if (sessionId.value === 0) {
        console.log('[TnfrStore] sessionId is 0, calling load()');
        await load();
        console.log('[TnfrStore] after load(), jobs hoursConfirmed:', jobs.value.map(j => ({ id: j.id, hoursConfirmed: j.hoursConfirmed })));
      }
    }
  }

  function clearSession() {
    startTime.value = "";
    activeJob.value = "";
    jobs.value = [];
    pendingJob.value = null;
    customerProgress.value = [];
    invoice.value = { paymentConfirmed: false, paymentInfo: '', lineItems: [], customerName: '', customerAddress: '', customerPhone: '', customerEmail: '' };
  }

  async function logServiceCall(): Promise<number> {
    // Get user info from auth
    let createdBy = "unknown";
    let createdByName = "Unknown User";
    try {
      const { pb } = await getApi();
      if (pb.authStore.record) {
        createdBy = pb.authStore.record.id || "unknown";
        createdByName = pb.authStore.record.name || pb.authStore.record.email || "Unknown User";
      }
    } catch (e) {
      console.error("[TnfrStore] Error getting user info for log:", e);
    }

    // Create a copy of the entire store state
    const storeSnapshot = {
      sessionId: sessionId.value,
      startTime: startTime.value,
      endTime: endTime.value,
      activeJob: activeJob.value,
      jobs: JSON.parse(JSON.stringify(jobs.value)),
      pendingJob: pendingJob.value ? JSON.parse(JSON.stringify(pendingJob.value)) : null,
      applyDiscount: applyDiscount.value,
      showPlan: showPlan.value,
      serviceCallsCount: serviceCallsCount.value,
      menusShownCount: menusShownCount.value,
      menusShownGoal: menusShownGoal.value,
      higherTierChosenCount: higherTierChosenCount.value,
      serviceCallsWithMenusShown: serviceCallsWithMenusShown.value,
      paymentMethod: paymentMethod.value,
      customerProgress: customerProgress.value,
      invoice: JSON.parse(JSON.stringify(invoice.value)),
      seenMenuJobIds: seenMenuJobIds.value,
      // Organization settings
      hourlyFee: hourlyFee.value,
      saDiscount: saDiscount.value,
      serviceCallFee: serviceCallFee.value,
      salesTax: salesTax.value,
      paymentPlanRate: paymentPlanRate.value,
      paymentPlanNumberPayments: paymentPlanNumberPayments.value,
      // Currency
      currency: currency.value,
      currencyPrefs: currencyPrefs.value,
    };

    const logId = await saveLog({
      log_type: "service_call",
      log_data: storeSnapshot,
      created_by: createdBy,
      created_by_name: createdByName,
    });

    console.log("[TnfrStore] Service call logged with ID:", logId);
    return logId;
  }

  function createInvoice() {
    // Build line items from jobs with selected offers
    const lineItems: InvoiceLineItem[] = jobs.value
      .filter(job => job.selectedOffer && job.selectedPrice)
      .map(job => {
        const price = parseFloat(job.selectedPrice) || 0;
        const discountPrice = price * 0.97; // Service agreement rate
        return {
          jobId: job.id,
          jobTitle: job.title || job.problem?.name || '',
          offerId: job.selectedOffer?.id || '',
          offerRefId: job.selectedOffer?.refId || '',
          menuName: job.menus?.[0]?.name || '',
          tierName: job.selectedTierName || '',
          tierTitle: job.selectedTierTitle || '',
          price,
          discountPrice,
          tierContent: job.selectedTierContent,
          offer: job.selectedOffer,
        };
      });

    // Calculate subtotal (full price)
    const subtotal = lineItems.reduce((sum, item) => sum + item.price, 0);

    // Calculate discount subtotal (with service agreement)
    const discountSubtotal = lineItems.reduce((sum, item) => sum + (item.discountPrice || item.price), 0);

    // Use discount price if service agreement is applied
    const total = applyDiscount.value ? discountSubtotal : subtotal;

    // Calculate monthly payment if payment plan is enabled
    const monthlyPayment = showPlan.value ? calculateMonthlyPayment(total) : null;

    return {
      lineItems,
      subtotal,
      discountSubtotal,
      total,
      monthlyPayment,
      numberOfPayments: showPlan.value ? paymentPlanNumberPayments.value : null,
      applyDiscount: applyDiscount.value,
      showPlan: showPlan.value,
      paymentConfirmed: invoice.value.paymentConfirmed,
      paymentInfo: invoice.value.paymentInfo,
      createdAt: new Date().toISOString(),
    };
  }

  async function addJob(problemId: string) {
    const { loadJobContentByProblemId } = await import("@/framework/jobContent");

    // Load job content from framework
    const jobContent = await loadJobContentByProblemId(problemId);
    if (!jobContent) {
      console.error('[TnfrStore] Could not load job content for problem:', problemId);
      return null;
    }

    // Generate job ID
    const jobId = Date.now().toString();

    if (jobs.value.length === 0 && !startTime.value) {
      startTime.value = new Date().toISOString();
    }

    // Calculate base hours from the lowest tier (band-aid)
    let baseHours = 0;
    const firstMenu = jobContent.menus?.[0];
    if (firstMenu?.tiers?.length > 0) {
      const bandAidTier = firstMenu.tiers[firstMenu.tiers.length - 1];
      baseHours = bandAidTier.offer?.costsTime?.reduce(
        (sum: number, cost: any) => sum + (cost.hours || 0),
        0
      ) || 0;
    }

    // Set as pending job (overwrites any existing pending job)
    pendingJob.value = {
      id: jobId,
      title: jobContent.problem.name || "",
      problem: jobContent.problem as unknown as ProblemsRecord,
      menus: jobContent.menus || [],
      baseHours,
      extraTime: 0,
      extraMaterial: 0,
      selectedOffer: {} as OffersRecord,
      selectedPrice: "",
      notes: [],
      hoursConfirmed: false,
    } as JobRecord;

    await save();
    return jobId;
  }

  async function removeJob(jobId: string) {
    const jobIndex = jobs.value.findIndex((e) => e.id === jobId);
    if (jobIndex !== -1) {
      jobs.value.splice(jobIndex, 1);

      // Also remove from invoice line items
      const lineItemIndex = invoice.value.lineItems.findIndex(item => item.jobId === jobId);
      if (lineItemIndex !== -1) {
        invoice.value.lineItems.splice(lineItemIndex, 1);
      }

      await save();
    }
  }

  async function confirmPendingJob() {
    if (!pendingJob.value) return null;

    // Move pending job to jobs array
    jobs.value.push(pendingJob.value);

    const jobId = pendingJob.value.id;

    // Clear pending job
    pendingJob.value = null;

    await save();
    return jobId;
  }

  async function clearPendingJob() {
    pendingJob.value = null;
    await save();
  }

  async function fetchMenuDataForProblem(problem: ProblemsRecord): Promise<any | null> {
    if (!problem.menus || !db.value) return null;
    try {
      let menuIds: string[] = [];
      if (typeof problem.menus === 'string') {
        try { menuIds = JSON.parse(problem.menus); } catch { menuIds = [problem.menus]; }
      } else if (Array.isArray(problem.menus)) {
        menuIds = problem.menus;
      }
      if (menuIds.length > 0) {
        return await db.value.menus.byMenuId(menuIds[0]) || null;
      }
    } catch (error) {
      console.error('[TnfrStore] Error fetching menu data:', error);
    }
    return null;
  }

  async function addProblemToJob(jobId: string, problem: ProblemsRecord) {
    const job = jobs.value.find((e) => e.id === jobId);
    if (job) {
      job.problem = problem;
      const menuData = await fetchMenuDataForProblem(problem);
      if (menuData) {
        job.menuData = menuData;
        if (menuData.tiers && menuData.tiers.length > 0) {
          const bandAidTier = menuData.tiers[menuData.tiers.length - 1];
          if (bandAidTier.offer && db.value) {
            try {
              const offerData = await db.value.offerData(bandAidTier.offer.id);
              if (offerData && offerData.costsTime) {
                job.baseHours = offerData.costsTime.reduce((sum: number, cost: any) => sum + (cost.hours || 0), 0);
              }
            } catch (error) {
              console.error('[TnfrStore] Error fetching offer data:', error);
            }
          }
        }
      }
      await save();
    }
  }

  async function updateExtraTime(jobId: string, hours: number) {
    // Check pendingJob first
    if (pendingJob.value?.id === jobId) {
      pendingJob.value = { ...pendingJob.value, extraTime: hours };
      await save();
      return;
    }
    // Then check jobs array
    const jobIndex = jobs.value.findIndex((e) => e.id === jobId);
    if (jobIndex !== -1) {
      jobs.value[jobIndex] = { ...jobs.value[jobIndex], extraTime: hours };
      await save();
    }
  }

  async function setSelectedOffer(jobId: string, offer: OffersRecord | null, price: string | null, tierName?: string | null, tierTitle?: string | null, tierBaseHours?: number | null, tierContent?: string[] | null) {
    // Check pendingJob first, then jobs array
    const job = pendingJob.value?.id === jobId ? pendingJob.value : jobs.value.find((e) => e.id === jobId);
    if (job) {
      job.selectedOffer = offer || {} as OffersRecord;
      job.selectedPrice = price || '';
      if (tierName) job.selectedTierName = tierName;
      else if (tierName === null) job.selectedTierName = undefined;
      if (tierTitle) job.selectedTierTitle = tierTitle;
      else if (tierTitle === null) job.selectedTierTitle = undefined;
      if (tierBaseHours !== undefined && tierBaseHours !== null) job.baseHours = tierBaseHours;
      if (tierContent) job.selectedTierContent = tierContent;
      else if (tierContent === null) job.selectedTierContent = undefined;

      // Update invoice line items
      const existingLineItemIndex = invoice.value.lineItems.findIndex(item => item.jobId === jobId);

      if (offer && price) {
        // Add or update line item
        const priceNum = parseFloat(price) || 0;
        const discountPrice = priceNum * 0.97; // Service agreement rate

        const lineItem: InvoiceLineItem = {
          jobId,
          jobTitle: job.title || job.problem?.name || '',
          offerId: offer.id,
          offerRefId: offer.refId || '',
          menuName: job.menus?.[0]?.name || '',
          tierName: tierName || '',
          tierTitle: tierTitle || '',
          price: priceNum,
          discountPrice,
          tierContent: tierContent || undefined,
          offer,
        };

        if (existingLineItemIndex !== -1) {
          invoice.value.lineItems[existingLineItemIndex] = lineItem;
        } else {
          invoice.value.lineItems.push(lineItem);
        }
      } else {
        // Remove line item when offer is cleared
        if (existingLineItemIndex !== -1) {
          invoice.value.lineItems.splice(existingLineItemIndex, 1);
        }
      }

      await save();
    }
  }

  async function setHoursConfirmed(jobId: string, confirmed: boolean) {
    console.log('[TnfrStore] setHoursConfirmed called:', { jobId, confirmed });

    // Helper to calculate tier prices
    const calculateTierPrices = async (job: JobRecord) => {
      if (confirmed && job.menus?.length > 0) {
        const calculatePrice = (await import("@/framework/calculatePrice")).default;

        for (const menu of job.menus) {
          for (const tier of menu.tiers) {
            const formula = {
              vars: {
                materialCostBase: 0,
                timeCostBase: 0,
                hourlyFee: hourlyFee.value,
                materialMarkup: 1.05,
                standardDiscount: 0,
              },
              derivedVars: {
                materialFee: 0,
                timeFee: 0,
                totalFee: 0,
                finalPrice: 0,
              },
              operations: [
                { op: "mult", in1: "timeCostBase", in2: "hourlyFee", out: "timeFee" },
                { op: "mult", in1: "materialCostBase", in2: "materialMarkup", out: "materialFee" },
                { op: "add", in1: "timeFee", in2: "materialFee", out: "totalFee" },
                { op: "sub", in1: "totalFee", in2: "standardDiscount", out: "finalPrice" },
              ],
            };

            const result = await calculatePrice(
              tier.offer.costsMaterial as any[],
              tier.offer.costsTime as any[],
              formula
            );

            // Add price to tier (includes converted values from prefs)
            (tier as any).price = result.finalPrice;
            (tier as any).priceConverted = result.finalPriceConverted;
          }
        }
      }
    };

    // Check pendingJob first
    if (pendingJob.value?.id === jobId) {
      console.log('[TnfrStore] Found pendingJob with id:', jobId);
      pendingJob.value = { ...pendingJob.value, hoursConfirmed: confirmed };
      console.log('[TnfrStore] Set hoursConfirmed to:', pendingJob.value.hoursConfirmed);
      await calculateTierPrices(pendingJob.value);
      await save();
      return;
    }

    // Then check jobs array
    console.log('[TnfrStore] Available job IDs:', jobs.value.map(j => j.id));
    const jobIndex = jobs.value.findIndex((e) => e.id === jobId);
    console.log('[TnfrStore] Found job at index:', jobIndex);
    if (jobIndex !== -1) {
      // Replace the job object to ensure reactivity
      jobs.value[jobIndex] = { ...jobs.value[jobIndex], hoursConfirmed: confirmed };
      const job = jobs.value[jobIndex];
      console.log('[TnfrStore] Set hoursConfirmed to:', job.hoursConfirmed);
      await calculateTierPrices(job);
      await save();
    }
  }

  async function setJobEdited(jobId: string, edited: boolean) {
    const job = jobs.value.find((e) => e.id === jobId);
    if (job) {
      job.edited = edited;
      await save();
    }
  }

  async function setJobTitle(jobId: string, title: string) {
    const job = jobs.value.find((e) => e.id === jobId);
    if (job) {
      job.title = title;
      await save();
    }
  }

  async function setJobContent(jobId: string, jobContent: JobContentHierarchy) {
    const job = jobs.value.find((e) => e.id === jobId);
    if (job) {
      job.problem = jobContent.problem as unknown as ProblemsRecord;
      job.menus = jobContent.menus || [];
      await save();
    }
  }

  async function addNote(jobId: string, note: string) {
    // Check pendingJob first, then jobs array
    const job = pendingJob.value?.id === jobId ? pendingJob.value : jobs.value.find((e) => e.id === jobId);
    if (job) {
      if (!job.notes) {
        job.notes = [];
      }
      if (!job.notes.includes(note)) {
        job.notes.push(note);
        await save();
      }
    }
  }

  async function removeNote(jobId: string, note: string) {
    // Check pendingJob first, then jobs array
    const job = pendingJob.value?.id === jobId ? pendingJob.value : jobs.value.find((e) => e.id === jobId);
    if (job && job.notes) {
      const noteIndex = job.notes.indexOf(note);
      if (noteIndex !== -1) {
        job.notes.splice(noteIndex, 1);
        await save();
      }
    }
  }

  // ============ Preferences actions ============
  async function getPreferences() {
    const dark = (await Preferences.get({ key: "darkMode" })).value;
    darkMode.value = dark === "true";
    const curr = (await Preferences.get({ key: "currency" })).value;
    currency.value = curr || "usd";
    const r = (await Preferences.get({ key: "role" })).value;
    role.value = r || "tech";
  }

  async function setPreference(key: string, value: string) {
    await Preferences.set({ key, value });
    await getPreferences();
    if (key === "darkMode") darkModeToggleCount.value++;

    // When setting currency, also persist full currency prefs to userPrefs table
    if (key === "currency") {
      await syncCurrencyPrefs(value);
    }
  }

  // Helper to build and store full currency prefs
  async function syncCurrencyPrefs(targetCurrency: string) {
    // Ensure we have currencies and rates loaded
    if (currencies.value.length === 0) {
      await getCurrencies();
    }
    if (exchangeRates.value.length === 0) {
      await getRates();
    }

    // Find the currency record to get the symbol
    const currencyRecord = currencies.value.find(
      (c) => c.refId?.toLowerCase() === targetCurrency.toLowerCase()
    );

    // Find the exchange rate for the target currency
    const rate = exchangeRates.value.find(
      (r) => r.quoteCurrency?.toLowerCase() === targetCurrency.toLowerCase()
    );

    const prefs: CurrencyPrefs = {
      baseCurrency: rate?.baseCurrency || "XAG",
      targetCurrency: targetCurrency.toUpperCase(),
      exchangeRate: rate?.rate ? parseFloat(rate.rate) : 1,
      symbol: currencyRecord?.symbol || "$",
    };

    currencyPrefs.value = prefs;
    await storeCurrency(prefs);
  }

  // ============ Currency actions ============
  async function getCurrencies() {
    await initDb();
    if (db.value) {
      currencies.value = (await db.value.currencies.getAll()) || [];
    }
  }

  async function getRates() {
    await initDb();
    if (db.value) {
      exchangeRates.value = (await db.value.currencies.getRates()) || [];
    }
  }

  // ============ Organization actions ============
  async function loadOrgVariables() {
    orgIsLoading.value = true;
    try {
      await initDb();
      if (db.value?.organizations) {
        const variables = await db.value.organizations.getCurrentVariables();
        if (variables) {
          hourlyFee.value = variables.hourlyFee ?? 5.262;
          saDiscount.value = variables.saDiscount ?? 1.5;
          serviceCallFee.value = variables.serviceCallFee ?? 2.0;
          salesTax.value = variables.salesTax ?? 1.08;
        }
      }
    } catch (error) {
      console.error("Failed to load organization variables:", error);
    } finally {
      orgIsLoading.value = false;
    }
  }

  // ============ Default jobs actions ============
  async function loadDefaultJob1() {
    const { loadJobContentByProblemRefId } = await import("@/framework/jobContent");
    const calculatePrice = (await import("@/framework/calculatePrice")).default;

    const jobContent = await loadJobContentByProblemRefId("PA1_problem");
    if (jobContent) {
      // Calculate price for each tier
      for (const menu of jobContent.menus) {
        for (const tier of menu.tiers) {
          const formula = {
            vars: {
              materialCostBase: 0,
              timeCostBase: 0,
              hourlyFee: hourlyFee.value,
              materialMarkup: 1.05,
              standardDiscount: 0,
            },
            derivedVars: {
              materialFee: 0,
              timeFee: 0,
              totalFee: 0,
              finalPrice: 0,
            },
            operations: [
              { op: "mult", in1: "timeCostBase", in2: "hourlyFee", out: "timeFee" },
              { op: "mult", in1: "materialCostBase", in2: "materialMarkup", out: "materialFee" },
              { op: "add", in1: "timeFee", in2: "materialFee", out: "totalFee" },
              { op: "sub", in1: "totalFee", in2: "standardDiscount", out: "finalPrice" },
            ],
          };

          const result = await calculatePrice(
            tier.offer.costsMaterial as any[],
            tier.offer.costsTime as any[],
            formula
          );

          // Add price to tier (includes converted values from prefs)
          (tier as any).price = result.finalPrice;
          (tier as any).priceConverted = result.finalPriceConverted;
        }
      }

      defaultJob1.value = jobContent;
      console.log('[TnfrStore] Loaded defaultJob1 with prices:', jobContent.problem.name);
    } else {
      console.error('[TnfrStore] Could not load defaultJob1 (PA1_problem)');
    }
    return jobContent;
  }

  // Returns the default job for menu display
  const nextJob = computed<JobContentHierarchy | null>(() => {
    return defaultJob1.value;
  });

  // Mark a menu as seen
  function markMenuSeen(jobId: string) {
    if (!seenMenuJobIds.value.includes(jobId)) {
      seenMenuJobIds.value.push(jobId);
    }
  }

  // Check if all menus have been seen
  const allMenusSeen = computed(() => {
    if (jobs.value.length === 0) return false;
    return jobs.value.every(job => seenMenuJobIds.value.includes(job.id));
  });

  // Check if any job has a selected offer
  const hasAnySelectedOffer = computed(() => {
    return jobs.value.some(job => job.selectedOffer?.id);
  });

  // Check if ready to go to payment (all menus seen + at least one offer selected)
  const canGoToPayment = computed(() => {
    return allMenusSeen.value && hasAnySelectedOffer.value;
  });

  // Load speed dial: top 5 frequent jobs + saved prefs
  async function loadSpeedDial(): Promise<void> {
    // Get top 5 most frequent jobs
    const jobCounts = await countJobs();
    const topRefIds = jobCounts.slice(0, 5).map(job => job.refId);

    // Load saved speed dial pref
    const savedPref = await getPref('speedDial');
    let savedRefIds: string[] = [];
    if (savedPref) {
      try {
        savedRefIds = JSON.parse(savedPref);
      } catch (e) {
        console.error('[TnfrStore] Error parsing speedDial pref:', e);
      }
    }

    // Combine: start with top frequent, add unique saved ones
    const combined = [...topRefIds];
    for (const refId of savedRefIds) {
      if (!combined.includes(refId)) {
        combined.push(refId);
      }
    }

    speedDial.value = combined;
  }

  // Add a problem to speed dial and save to prefs
  async function addToSpeedDial(refId: string): Promise<void> {
    if (!speedDial.value.includes(refId)) {
      speedDial.value.push(refId);
    }

    // Load current saved pref
    const savedPref = await getPref('speedDial');
    let savedRefIds: string[] = [];
    if (savedPref) {
      try {
        savedRefIds = JSON.parse(savedPref);
      } catch (e) {
        savedRefIds = [];
      }
    }

    // Add to saved if not already there
    if (!savedRefIds.includes(refId)) {
      savedRefIds.push(refId);
      await setPref('speedDial', JSON.stringify(savedRefIds));
    }
  }

  // Remove a problem from speed dial and update prefs
  async function removeFromSpeedDial(refId: string): Promise<void> {
    const index = speedDial.value.indexOf(refId);
    if (index !== -1) {
      speedDial.value.splice(index, 1);
    }

    // Update saved pref
    const savedPref = await getPref('speedDial');
    let savedRefIds: string[] = [];
    if (savedPref) {
      try {
        savedRefIds = JSON.parse(savedPref);
      } catch (e) {
        savedRefIds = [];
      }
    }

    const savedIndex = savedRefIds.indexOf(refId);
    if (savedIndex !== -1) {
      savedRefIds.splice(savedIndex, 1);
      await setPref('speedDial', JSON.stringify(savedRefIds));
    }
  }

  return {
    // Session state
    sessionId,
    startTime,
    activeJob,
    jobs,
    pendingJob,
    endTime,
    applyDiscount,
    showPlan,
    serviceCallsCount,
    menusShownCount,
    menusShownGoal,
    higherTierChosenCount,
    serviceCallsWithMenusShown,
    themeColors,
    menuThemeColors,
    problems,
    problemTags,
    selectedTags,
    appliedSearchQuery,
    selectionHistory,
    paymentMethod,
    editMode,
    secretMode,
    customerProgress,
    invoice,
    seenMenuJobIds,
    speedDial,
    loadSpeedDial,
    addToSpeedDial,
    removeFromSpeedDial,
    allMenusSeen,
    hasAnySelectedOffer,
    canGoToPayment,
    markMenuSeen,

    // Session actions
    load,
    save,
    clear: clearSession,
    logServiceCall,
    createInvoice,
    addJob,
    removeJob,
    confirmPendingJob,
    clearPendingJob,
    addProblemToJob,
    updateExtraTime,
    setSelectedOffer,
    setHoursConfirmed,
    setJobEdited,
    setJobTitle,
    setJobContent,
    addNote,
    removeNote,

    // Preferences state
    darkMode,
    currency,
    role,
    darkModeToggleCount,
    getPreferences,
    setPreference,

    // Currency state
    currencies,
    exchangeRates,
    currencyPrefs,
    getCurrencies,
    getRates,
    syncCurrencyPrefs,

    // Organization state
    orgIsLoading,
    hourlyFee,
    saDiscount,
    serviceCallFee,
    salesTax,
    paymentPlanRate,
    paymentPlanNumberPayments,
    calculateMonthlyPayment,
    loadOrgVariables,

    // Menu bullet functions
    getMenuBullets,
    getTierBullets,
    addMenuBullet,
    editMenuBullet,
    removeMenuBullet,
    assignMenuBulletToTier,
    removeBulletFromTier,

    // Default jobs
    defaultJob1,
    loadDefaultJob1,
    nextJob,
  };
});