← back to Rc Fm Callnotes

lib/fm.js

91 lines

// Thin wrapper over the filemaker-mcp Data API client (whitelisted files only).
// Loads that project's .env so Claris credentials stay in one place.
import { readFileSync } from 'node:fs';

const FM_DIR = '/Users/macstudio3/Projects/filemaker-mcp';
for (const line of readFileSync(`${FM_DIR}/.env`, 'utf8').split('\n')) {
  const m = line.match(/^([A-Z_]+)=(.*)$/);
  if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
}

const fm = await import(`${FM_DIR}/src/fm-client.js`);
export const { findRecords, createRecord, updateRecord, listRecords, getRecord } = fm;

export const CLIENTS_LAYOUT = 'Main Menu Copy';   // exposes Phone + Notes (writable)
export const INVOICE_LAYOUT = 'PACKING LIST !!';  // exposes Invoice + Internal notes

// Digits-only canonical form, US numbers reduced to 10 digits.
export function normPhone(raw) {
  let d = String(raw || '').replace(/\D/g, '');
  if (d.length === 11 && d.startsWith('1')) d = d.slice(1);
  return d;
}

// Common ways a 10-digit US number appears in the Clients file.
export function phoneVariants(digits10) {
  if (digits10.length !== 10) return [`*${digits10}*`];
  const a = digits10.slice(0, 3), b = digits10.slice(3, 6), c = digits10.slice(6);
  return [
    `*${a}-${b}-${c}*`,
    `*(${a}) ${b}-${c}*`,
    `*(${a})${b}-${c}*`,
    `*${a}.${b}.${c}*`,
    `*${a} ${b} ${c}*`,
    `*${a}${b}${c}*`,
  ];
}

// Find a client by phone number; returns first match or null.
export async function findClientByPhone(rawNumber) {
  const digits = normPhone(rawNumber);
  if (digits.length < 7) return null;
  const query = phoneVariants(digits).map((v) => ({ Phone: v }));
  try {
    const res = await findRecords('Clients', CLIENTS_LAYOUT, query, { limit: 3 });
    return res.records?.[0] || null;
  } catch (e) {
    if (e.fmCode === '401') return null; // 401 = no records match
    throw e;
  }
}

// Append text to a client's Notes (read-modify-write; newest entry on top).
export async function appendClientNote(recordId, existingNotes, entry) {
  const merged = entry + (existingNotes ? `\n\n${existingNotes}` : '');
  return updateRecord('Clients', CLIENTS_LAYOUT, recordId, { Notes: merged }, { dryRun: false });
}

export async function createClientFromCall({ name, phone, note }) {
  const fieldData = {
    Company: name || 'Unknown caller',
    Name: name || '',
    Phone: phone,
    Notes: note,
    'Date Added': new Date().toLocaleDateString('en-US'),
  };
  // 'Date Added' isn't on Main Menu Copy — only send fields the layout exposes.
  delete fieldData['Date Added'];
  return createRecord('Clients', CLIENTS_LAYOUT, fieldData, { dryRun: false });
}

export async function findInvoiceByNumber(invoiceNo) {
  try {
    const res = await findRecords('invoice', INVOICE_LAYOUT, { Invoice: `==${invoiceNo}` }, { limit: 2 });
    return res.records?.[0] || null;
  } catch (e) {
    if (e.fmCode === '401') return null;
    throw e;
  }
}

// The PACKING LIST !! layout exposes the field as "Internal notes(1)" on reads.
export function invoiceNotesOf(record) {
  const f = record?.fieldData || {};
  return f['Internal notes'] ?? f['Internal notes(1)'] ?? '';
}

export async function appendInvoiceInternalNote(recordId, existingNotes, entry) {
  const merged = entry + (existingNotes ? `\n${existingNotes}` : '');
  return updateRecord('invoice', INVOICE_LAYOUT, recordId, { 'Internal notes': merged }, { dryRun: false });
}