Hello from MCP server
<template>
<div class="tnfr-view">
<div class="top-row">
<div class="top-row-left">
<button type="button" :class="{ active: activeStore === 'tnfr' }" @click="handleStoreClick('tnfr')">tnfrStore</button>
<button type="button" :class="{ active: activeStore === 'session' }" @click="handleStoreClick('session')">sessionStore</button>
<button type="button" :class="{ active: activeStore === 'preferences' }" @click="handleStoreClick('preferences')">preferencesStore</button>
<button type="button" :class="{ active: activeStore === 'currency' }" @click="handleStoreClick('currency')">currencyStore</button>
<button type="button" :class="{ active: activeStore === 'organization' }" @click="handleStoreClick('organization')">organizationStore</button>
</div>
<div class="top-row-right">
<button type="button" @click="handleClear">Clear</button>
</div>
</div>
<!-- Jobs Dropdown -->
<div class="jobs-dropdown-section">
<label for="jobs-dropdown">Jobs (click to copy ID):</label>
<select id="jobs-dropdown" @change="handleJobSelect($event)">
<option value="">-- Select a job --</option>
<option v-for="problem in sortedProblems" :key="problem.id" :value="problem.id">
{{ problem.name }}
</option>
</select>
<span v-if="copiedId" class="copied-message">Copied: {{ copiedId }}</span>
</div>
<!-- Function Caller for each store -->
<StoreFunctionCaller
v-if="activeStore === 'tnfr'"
:function-list="tnfrFunctionDefs"
/>
<StoreFunctionCaller
v-else-if="activeStore === 'session'"
:function-list="sessionFunctionDefs"
/>
<StoreFunctionCaller
v-else-if="activeStore === 'preferences'"
:function-list="preferencesFunctionDefs"
/>
<StoreFunctionCaller
v-else-if="activeStore === 'currency'"
:function-list="currencyFunctionDefs"
/>
<StoreFunctionCaller
v-else-if="activeStore === 'organization'"
:function-list="organizationFunctionDefs"
/>
<div v-if="jsonData" class="json-block">
<div class="json-controls">
<button type="button" @click="expandAllJson">Expand All</button>
<button type="button" @click="collapseAllJson">Collapse All</button>
</div>
<pre><code><json-node :key="jsonKey" :data="jsonData" :indent="0" :start-collapsed="!jsonExpandAll" :expand-all="jsonExpandMode" /></code></pre>
</div>
</div>
</template>
<script lang="ts">
import { ref, computed, defineComponent, onMounted } from "vue";
import { useSessionStore } from "@/stores/session";
import { usePreferencesStore } from "@/stores/preferences";
import { useCurrencyStore } from "@/stores/currency";
import { useOrganizationStore } from "@/stores/organization";
import { useTnfrStore } from "@/stores/tnfr";
import StoreFunctionCaller, { type FunctionDefinition } from "@/components/StoreFunctionCaller.vue";
import JsonNode from "@/components/JsonViewer.vue";
import { loadDirectory, type DirectoryData } from "@/framework/directory";
import type { ProblemsRecord } from "@/pocketbase-types";
export default defineComponent({
name: "TNFR",
components: { JsonNode, StoreFunctionCaller },
setup() {
const sessionStore = useSessionStore();
const preferencesStore = usePreferencesStore();
const currencyStore = useCurrencyStore();
const organizationStore = useOrganizationStore();
const tnfrStore = useTnfrStore();
const activeStore = ref<string | null>(null);
const jsonExpandAll = ref(true);
const jsonExpandMode = ref(false);
const jsonKey = ref(0);
const storeFunctions = ref<string[]>([]);
// Jobs dropdown state
const allProblems = ref<ProblemsRecord[]>([]);
const copiedId = ref<string | null>(null);
// Sorted problems alphabetically
const sortedProblems = computed(() => {
return [...allProblems.value].sort((a, b) =>
(a.name || '').localeCompare(b.name || '')
);
});
// Load problems on mount
onMounted(async () => {
try {
const directoryData = await loadDirectory();
allProblems.value = directoryData.problems;
} catch (error) {
console.error('[TNFR] Error loading directory:', error);
}
});
// Handle job selection - copy ID to clipboard
async function handleJobSelect(event: Event) {
const select = event.target as HTMLSelectElement;
const problemId = select.value;
if (!problemId) return;
try {
await navigator.clipboard.writeText(problemId);
copiedId.value = problemId;
// Reset selection
select.value = '';
// Clear copied message after 2 seconds
setTimeout(() => {
copiedId.value = null;
}, 2000);
} catch (error) {
console.error('[TNFR] Error copying to clipboard:', error);
}
}
// Reactive computed property that auto-updates when store changes
const jsonData = computed(() => {
if (!activeStore.value) return null;
switch (activeStore.value) {
case 'tnfr':
return {
...tnfrStore.$state,
nextJob: tnfrStore.nextJob,
};
case 'session':
return {
jobs: sessionStore.jobs,
appProgress: sessionStore.appProgress,
customerProgress: sessionStore.customerProgress,
secretMode: sessionStore.secretMode,
};
case 'preferences':
return {
darkMode: preferencesStore.darkMode,
currency: preferencesStore.currency,
role: preferencesStore.role,
darkModeToggleCount: preferencesStore.darkModeToggleCount,
};
case 'currency':
return {
currencies: currencyStore.currencies,
exchangeRates: currencyStore.exchangeRates,
};
case 'organization':
return {
hourlyFee: organizationStore.hourlyFee,
saDiscount: organizationStore.saDiscount,
serviceCallFee: organizationStore.serviceCallFee,
salesTax: organizationStore.salesTax,
isLoading: organizationStore.isLoading,
};
default:
return null;
}
});
function getStoreFunctions(store: any): string[] {
return Object.keys(store).filter(key => typeof store[key] === 'function').sort();
}
function expandAllJson() {
jsonExpandAll.value = true;
jsonExpandMode.value = true;
jsonKey.value++;
}
function collapseAllJson() {
jsonExpandAll.value = false;
jsonExpandMode.value = true;
jsonKey.value++;
}
function handleClear() {
activeStore.value = null;
storeFunctions.value = [];
}
async function handleStoreClick(storeName: string) {
handleClear();
activeStore.value = storeName;
switch (storeName) {
case 'session':
await sessionStore.load();
storeFunctions.value = getStoreFunctions(sessionStore);
break;
case 'preferences':
await preferencesStore.getPreferences();
storeFunctions.value = getStoreFunctions(preferencesStore);
break;
case 'currency':
await currencyStore.getCurrencies();
await currencyStore.getRates();
storeFunctions.value = getStoreFunctions(currencyStore);
break;
case 'organization':
await organizationStore.loadVariables();
storeFunctions.value = getStoreFunctions(organizationStore);
break;
case 'tnfr':
await tnfrStore.load();
await tnfrStore.getPreferences();
await tnfrStore.getCurrencies();
await tnfrStore.getRates();
await tnfrStore.loadOrgVariables();
storeFunctions.value = getStoreFunctions(tnfrStore);
break;
}
}
// Define session store function definitions
const sessionFunctionDefs = computed<FunctionDefinition[]>(() => [
{
name: 'load',
description: 'Load session state from database',
handler: () => sessionStore.load(),
},
{
name: 'save',
description: 'Save current session state to database',
handler: () => sessionStore.save(),
},
{
name: 'clear',
description: 'Clear all session data',
handler: () => sessionStore.clear(),
},
{
name: 'addJob',
description: 'Add a new job to the session',
params: [
{ name: 'title', type: 'string', required: true, placeholder: 'Job title' },
],
handler: (title: string) => sessionStore.addJob(title),
},
{
name: 'updateCustomerProgress',
description: 'Update customer progress at index',
params: [
{ name: 'stepIndex', type: 'number', required: true, placeholder: '0' },
{ name: 'value', type: 'string', required: true, placeholder: 'true or step name' },
],
handler: (stepIndex: number, value: string | boolean) => sessionStore.updateCustomerProgress(stepIndex, value),
},
]);
// Define preferences store function definitions
const preferencesFunctionDefs = computed<FunctionDefinition[]>(() => [
{
name: 'getPreferences',
description: 'Load all preferences from storage',
handler: () => preferencesStore.getPreferences(),
},
{
name: 'getPreference',
description: 'Get a single preference value',
params: [
{ name: 'key', type: 'string', required: true, placeholder: 'e.g., darkMode, currency, role' },
],
handler: (key: string) => preferencesStore.getPreference(key),
},
{
name: 'setPreference',
description: 'Set a preference value',
params: [
{ name: 'key', type: 'string', required: true, placeholder: 'e.g., darkMode, currency, role' },
{ name: 'value', type: 'string', required: true, placeholder: 'Preference value' },
],
handler: (key: string, value: string) => preferencesStore.setPreference(key, value),
},
{
name: 'syncCurrencyPrefs',
description: 'Sync currency preferences to userPrefs table',
params: [
{ name: 'targetCurrency', type: 'string', required: true, placeholder: 'e.g., usd, gbp' },
],
handler: (targetCurrency: string) => preferencesStore.syncCurrencyPrefs(targetCurrency),
},
{
name: 'resetDarkModeToggleCount',
description: 'Reset the dark mode toggle counter',
handler: () => preferencesStore.resetDarkModeToggleCount(),
},
]);
// Define currency store function definitions
const currencyFunctionDefs = computed<FunctionDefinition[]>(() => [
{
name: 'getCurrencies',
description: 'Load available currencies from database',
handler: () => currencyStore.getCurrencies(),
},
{
name: 'getRates',
description: 'Load exchange rates from database',
handler: () => currencyStore.getRates(),
},
{
name: 'syncCurrencyPrefs',
description: 'Sync currency preferences to userPrefs table (one-way)',
params: [
{ name: 'targetCurrency', type: 'string', required: true, placeholder: 'e.g., usd, gbp, eur' },
],
handler: (targetCurrency: string) => currencyStore.syncCurrencyPrefs(targetCurrency),
},
]);
// Define organization store function definitions
const organizationFunctionDefs = computed<FunctionDefinition[]>(() => [
{
name: 'loadVariables',
description: 'Load organization variables (hourly fee, tax, etc.)',
handler: () => organizationStore.loadVariables(),
},
{
name: 'setHourlyFee',
description: 'Set the hourly fee rate',
params: [
{ name: 'fee', type: 'number', required: true, placeholder: '5.262' },
],
handler: (fee: number) => { organizationStore.hourlyFee = fee; return { hourlyFee: fee }; },
},
{
name: 'setSaDiscount',
description: 'Set the service agreement discount',
params: [
{ name: 'discount', type: 'number', required: true, placeholder: '1.5' },
],
handler: (discount: number) => { organizationStore.saDiscount = discount; return { saDiscount: discount }; },
},
{
name: 'setServiceCallFee',
description: 'Set the service call fee',
params: [
{ name: 'fee', type: 'number', required: true, placeholder: '2.0' },
],
handler: (fee: number) => { organizationStore.serviceCallFee = fee; return { serviceCallFee: fee }; },
},
{
name: 'setSalesTax',
description: 'Set the sales tax rate',
params: [
{ name: 'tax', type: 'number', required: true, placeholder: '1.08' },
],
handler: (tax: number) => { organizationStore.salesTax = tax; return { salesTax: tax }; },
},
]);
// Define tnfr store function definitions with parameters
const tnfrFunctionDefs = computed<FunctionDefinition[]>(() => [
{
name: 'load',
description: 'Load session state from database',
handler: () => tnfrStore.load(),
},
{
name: 'save',
description: 'Save current session state to database',
handler: () => tnfrStore.save(),
},
{
name: 'clear',
description: 'Clear all session data',
handler: () => tnfrStore.clear(),
},
{
name: 'addJob',
description: 'Add a new job to the cart by problem ID (loads job content automatically)',
params: [
{ name: 'problemId', type: 'string', required: true, placeholder: 'Problem ID from jobs dropdown' },
],
handler: (problemId: string) => tnfrStore.addJob(problemId),
},
{
name: 'removeJob',
description: 'Remove a job from the cart',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID to remove' },
],
handler: (jobId: string) => tnfrStore.removeJob(jobId),
},
{
name: 'setJobTitle',
description: 'Set the title for a job',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'title', type: 'string', required: true, placeholder: 'New title' },
],
handler: (jobId: string, title: string) => tnfrStore.setJobTitle(jobId, title),
},
{
name: 'setJobEdited',
description: 'Mark a job as edited',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'edited', type: 'boolean', required: true },
],
handler: (jobId: string, edited: boolean) => tnfrStore.setJobEdited(jobId, edited),
},
{
name: 'setHoursConfirmed',
description: 'Mark hours as confirmed for a job',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'confirmed', type: 'boolean', required: true },
],
handler: (jobId: string, confirmed: boolean) => tnfrStore.setHoursConfirmed(jobId, confirmed),
},
{
name: 'updateExtraTime',
description: 'Update extra time hours for a job',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'hours', type: 'number', required: true, placeholder: '0' },
],
handler: (jobId: string, hours: number) => tnfrStore.updateExtraTime(jobId, hours),
},
{
name: 'getPreferences',
description: 'Load user preferences from storage',
handler: () => tnfrStore.getPreferences(),
},
{
name: 'setPreference',
description: 'Set a user preference',
params: [
{ name: 'key', type: 'string', required: true, placeholder: 'e.g., darkMode, currency, role' },
{ name: 'value', type: 'string', required: true, placeholder: 'Preference value' },
],
handler: (key: string, value: string) => tnfrStore.setPreference(key, value),
},
{
name: 'getCurrencies',
description: 'Load available currencies from database',
handler: () => tnfrStore.getCurrencies(),
},
{
name: 'getRates',
description: 'Load exchange rates from database',
handler: () => tnfrStore.getRates(),
},
{
name: 'loadOrgVariables',
description: 'Load organization variables (hourly fee, tax, etc.)',
handler: () => tnfrStore.loadOrgVariables(),
},
{
name: 'loadDefaultJob1',
description: 'Load PA1 "Fixture drain cleaning interior" into defaultJob1',
handler: () => tnfrStore.loadDefaultJob1(),
},
{
name: 'createInvoice',
description: 'Create and return invoice data with line items, totals, and payment plan info',
handler: () => tnfrStore.createInvoice(),
},
{
name: 'addProblemToJob',
description: 'Add a problem to an existing job (requires ProblemsRecord object - use programmatically)',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
],
handler: () => 'This function requires a ProblemsRecord object. Use programmatically via tnfrStore.addProblemToJob(jobId, problemRecord)',
},
{
name: 'setSelectedOffer',
description: 'Set the selected offer for a job (also updates invoice)',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'offerId', type: 'string', required: false, placeholder: 'Offer ID (null to clear)' },
{ name: 'price', type: 'string', required: false, placeholder: 'Price' },
{ name: 'tierName', type: 'string', required: false, placeholder: 'Tier name' },
{ name: 'tierTitle', type: 'string', required: false, placeholder: 'Tier title' },
],
handler: (jobId: string, offerId: string, price: string, tierName: string, tierTitle: string) =>
tnfrStore.setSelectedOffer(jobId, offerId ? { id: offerId } as any : null, price || null, tierName || null, tierTitle || null),
},
{
name: 'setJobContent',
description: 'Set the content/menus for a job',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'content', type: 'string', required: true, placeholder: 'JSON content object' },
],
handler: (jobId: string, content: string) => {
try {
const parsed = JSON.parse(content);
return tnfrStore.setJobContent(jobId, parsed);
} catch (e) {
console.error('Invalid JSON:', e);
return Promise.reject('Invalid JSON content');
}
},
},
{
name: 'calculateMonthlyPayment',
description: 'Calculate monthly payment using amortization formula',
params: [
{ name: 'principal', type: 'number', required: true, placeholder: 'Loan amount (principal)' },
],
handler: (principal: number) => tnfrStore.calculateMonthlyPayment(principal),
},
{
name: 'getMenuBullets',
description: 'Get all unique bullet points from all tiers in a job\'s menu',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
],
handler: (jobId: string) => tnfrStore.getMenuBullets(jobId),
},
{
name: 'getTierBullets',
description: 'Get bullet points for a specific tier',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'tierId', type: 'string', required: true, placeholder: 'Tier ID' },
],
handler: (jobId: string, tierId: string) => tnfrStore.getTierBullets(jobId, tierId),
},
{
name: 'addMenuBullet',
description: 'Add a new bullet to the menu pool (adds to all tiers)',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'content', type: 'string', required: true, placeholder: 'Bullet content' },
],
handler: (jobId: string, content: string) => tnfrStore.addMenuBullet(jobId, content),
},
{
name: 'editMenuBullet',
description: 'Edit a bullet\'s content (updates in all tiers)',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'bulletId', type: 'string', required: true, placeholder: 'Bullet ID' },
{ name: 'newContent', type: 'string', required: true, placeholder: 'New content' },
],
handler: (jobId: string, bulletId: string, newContent: string) => tnfrStore.editMenuBullet(jobId, bulletId, newContent),
},
{
name: 'removeMenuBullet',
description: 'Remove a bullet from menu and all tiers',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'bulletId', type: 'string', required: true, placeholder: 'Bullet ID' },
],
handler: (jobId: string, bulletId: string) => tnfrStore.removeMenuBullet(jobId, bulletId),
},
{
name: 'assignMenuBulletToTier',
description: 'Assign a bullet from menu pool to a specific tier',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'tierId', type: 'string', required: true, placeholder: 'Tier ID' },
{ name: 'bulletId', type: 'string', required: true, placeholder: 'Bullet ID' },
],
handler: (jobId: string, tierId: string, bulletId: string) => tnfrStore.assignMenuBulletToTier(jobId, tierId, bulletId),
},
{
name: 'removeBulletFromTier',
description: 'Remove a bullet from a specific tier only (keeps in menu pool)',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'tierId', type: 'string', required: true, placeholder: 'Tier ID' },
{ name: 'bulletId', type: 'string', required: true, placeholder: 'Bullet ID' },
],
handler: (jobId: string, tierId: string, bulletId: string) => tnfrStore.removeBulletFromTier(jobId, tierId, bulletId),
},
{
name: 'addNote',
description: 'Add a note to a job',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'note', type: 'string', required: true, placeholder: 'Note text' },
],
handler: (jobId: string, note: string) => tnfrStore.addNote(jobId, note),
},
{
name: 'removeNote',
description: 'Remove a note from a job',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
{ name: 'note', type: 'string', required: true, placeholder: 'Note text to remove' },
],
handler: (jobId: string, note: string) => tnfrStore.removeNote(jobId, note),
},
{
name: 'markMenuSeen',
description: 'Mark a menu as seen for a job',
params: [
{ name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
],
handler: (jobId: string) => tnfrStore.markMenuSeen(jobId),
},
{
name: 'syncCurrencyPrefs',
description: 'Sync currency preferences to userPrefs table (one-way)',
params: [
{ name: 'targetCurrency', type: 'string', required: true, placeholder: 'e.g., usd, gbp, eur' },
],
handler: (targetCurrency: string) => tnfrStore.syncCurrencyPrefs(targetCurrency),
},
]);
return {
jsonData,
activeStore,
jsonExpandAll,
jsonExpandMode,
jsonKey,
storeFunctions,
tnfrFunctionDefs,
sessionFunctionDefs,
preferencesFunctionDefs,
currencyFunctionDefs,
organizationFunctionDefs,
sortedProblems,
copiedId,
expandAllJson,
collapseAllJson,
handleClear,
handleStoreClick,
handleJobSelect,
};
},
});
</script>
<style scoped>
.tnfr-view {
padding: 16px;
padding-bottom: 100px;
font-family: system-ui, sans-serif;
min-height: 100vh;
overflow-y: auto;
}
.top-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.top-row-left,
.top-row-right {
display: flex;
gap: 8px;
}
.jobs-dropdown-section {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
padding: 12px;
background: var(--ion-background-color-step-50, #f5f5f5);
border: 1px solid var(--ion-color-step-200, #ddd);
border-radius: 4px;
}
.jobs-dropdown-section label {
font-size: 14px;
font-weight: 500;
color: var(--ion-text-color);
}
.jobs-dropdown-section select {
flex: 1;
max-width: 400px;
padding: 8px 12px;
font-size: 14px;
border: 1px solid var(--ion-color-step-300, #ccc);
border-radius: 4px;
background: var(--ion-background-color, #fff);
color: var(--ion-text-color);
cursor: pointer;
}
.copied-message {
font-size: 12px;
color: var(--ion-color-success);
font-weight: 500;
}
button {
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
background: var(--ion-background-color-step-100, #f0f0f0);
border: 1px solid var(--ion-color-step-300, #ccc);
color: var(--ion-text-color, #000);
border-radius: 4px;
}
button:hover {
background: var(--ion-background-color-step-150, #e0e0e0);
}
button.active {
background: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
}
.functions-block {
margin-top: 16px;
padding: 12px;
background: var(--ion-background-color-step-50, #f5f5f5);
border: 1px solid var(--ion-color-step-200, #ddd);
border-radius: 4px;
}
.functions-block h3 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: var(--ion-text-color);
}
.functions-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.functions-list code {
padding: 4px 8px;
background: var(--ion-background-color-step-100, #e8e8e8);
border-radius: 4px;
font-size: 12px;
color: var(--ion-color-primary);
border-color: var(--ion-color-primary-shade);
}
.json-block {
margin-top: 16px;
}
.json-block pre {
margin-top: 0;
}
.json-controls {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.json-controls button {
padding: 4px 10px;
font-size: 12px;
}
pre {
margin-top: 16px;
padding: 12px;
background: var(--ion-background-color-step-50, #f5f5f5);
border: 1px solid var(--ion-color-step-200, #ddd);
border-radius: 4px;
overflow-x: auto;
}
code {
font-family: monospace;
font-size: 12px;
white-space: pre;
color: var(--ion-text-color);
}
</style>