← back to Filemaker Mcp
chore: lint, refactor (shared fm-script-helpers), v0.2.0 (session close)
a4db9cad346fd88e987f13ecdea6d01f93f502b6 · 2026-07-02 08:08:54 -0700 · Steve
Files touched
A lib/fm-script-helpers.jsM package-lock.jsonM package.jsonM scripts/new-invoice-monitor.mjsM scripts/sales-summary.mjs
Diff
commit a4db9cad346fd88e987f13ecdea6d01f93f502b6
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 2 08:08:54 2026 -0700
chore: lint, refactor (shared fm-script-helpers), v0.2.0 (session close)
---
lib/fm-script-helpers.js | 63 +++++++++++++++++++++++++++++++++++++++++
package-lock.json | 4 +--
package.json | 2 +-
scripts/new-invoice-monitor.mjs | 26 +++++------------
scripts/sales-summary.mjs | 29 +++++--------------
5 files changed, 80 insertions(+), 44 deletions(-)
diff --git a/lib/fm-script-helpers.js b/lib/fm-script-helpers.js
new file mode 100644
index 0000000..32cf8e7
--- /dev/null
+++ b/lib/fm-script-helpers.js
@@ -0,0 +1,63 @@
+// Shared helpers for scripts/sales-summary.mjs and scripts/new-invoice-monitor.mjs.
+// No external dependencies — uses only Node built-ins already available in the scripts.
+
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+/**
+ * Load a `.env` file adjacent to the package root into `process.env`.
+ * Silently skips keys already present (process env wins over file).
+ * Safe to call if the file is absent (launchd / shell already exports the vars).
+ *
+ * @param {string} importMetaUrl - pass `import.meta.url` from the caller
+ * so the root is resolved correctly regardless of cwd.
+ */
+export function loadEnv(importMetaUrl) {
+ const root = join(dirname(fileURLToPath(importMetaUrl)), '..');
+ try {
+ for (const line of readFileSync(join(root, '.env'), 'utf8').split('\n')) {
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
+ if (m && !(m[1] in process.env)) {
+ process.env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
+ }
+ }
+ } catch { /* env may already be exported (e.g. launchd) */ }
+}
+
+/** @type {'America/Los_Angeles'} */
+export const TZ = 'America/Los_Angeles';
+
+/**
+ * Return the current Pacific-time {y, m, d} from a Date.
+ * Uses `en-CA` locale so formatToParts gives unambiguous year/month/day.
+ *
+ * @param {Date} d
+ * @returns {{ y: number, m: number, d: number }}
+ */
+export function ptYMD(d) {
+ const parts = new Intl.DateTimeFormat('en-CA', {
+ timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit',
+ }).formatToParts(d);
+ const get = (type) => parts.find((x) => x.type === type).value;
+ return { y: +get('year'), m: +get('month'), d: +get('day') };
+}
+
+/**
+ * Parse a value to a finite number; returns 0 for anything non-numeric.
+ * @param {unknown} v
+ * @returns {number}
+ */
+export function num(v) {
+ const n = parseFloat(v);
+ return Number.isFinite(n) ? n : 0;
+}
+
+/**
+ * Format a number as USD, e.g. `$1,234.56`.
+ * @param {number} n
+ * @returns {string}
+ */
+export function money(n) {
+ return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+}
diff --git a/package-lock.json b/package-lock.json
index bb34578..3d10aad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "filemaker-mcp",
- "version": "0.1.1",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "filemaker-mcp",
- "version": "0.1.1",
+ "version": "0.2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"amazon-cognito-identity-js": "^6.3.12",
diff --git a/package.json b/package.json
index b2ca7b9..b543d43 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "filemaker-mcp",
- "version": "0.1.1",
+ "version": "0.2.0",
"private": true,
"type": "module",
"description": "MCP server + CLI for reading and (confirmed) updating Designer Wallcoverings' FileMaker Cloud files (Clients, Invoice, WALLPAPER) via the FileMaker Data API.",
diff --git a/scripts/new-invoice-monitor.mjs b/scripts/new-invoice-monitor.mjs
index 330c8f5..abee875 100644
--- a/scripts/new-invoice-monitor.mjs
+++ b/scripts/new-invoice-monitor.mjs
@@ -9,16 +9,12 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { findRecords } from '../src/fm-client.js';
+import { loadEnv, ptYMD, num, money } from '../lib/fm-script-helpers.js';
+
+loadEnv(import.meta.url);
const __dir = dirname(fileURLToPath(import.meta.url));
-try {
- for (const line of readFileSync(join(__dir, '..', '.env'), 'utf8').split('\n')) {
- const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
- if (m && !(m[1] in process.env)) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
- }
-} catch { /* env may already be exported */ }
-const TZ = 'America/Los_Angeles';
const DB = 'invoice';
const LAYOUT = 'GRAND TOTAL SALES';
const CHANNEL = process.env.SALES_SUMMARY_CHANNEL || 'C06D9C2PG1K'; // #new-order
@@ -26,17 +22,9 @@ const DRY_RUN = process.argv.includes('--dry-run');
const CURSOR = join(__dir, '..', 'data', '.last-invoice');
const EXCLUDE = new Set(['999999']);
-const num = (v) => { const n = parseFloat(v); return Number.isFinite(n) ? n : 0; };
-const money = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
-
-function ptYMD(d) {
- const p = new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(d);
- const g = (t) => p.find((x) => x.type === t).value;
- return { y: +g('year'), m: +g('month'), d: +g('day') };
-}
-
async function post(text) {
const token = process.env.SLACK_BOT_TOKEN;
+ if (!token) throw new Error('SLACK_BOT_TOKEN not set.');
const res = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json; charset=utf-8' },
@@ -77,9 +65,9 @@ async function main() {
let maxPosted = lastSeen;
for (const { n, f } of fresh) {
- const client = String(f['Sold Company Name Lookup'] || 'Customer').replace(/\s+$/, '');
+ const client = String(f['Sold Company Name Lookup'] || 'Customer').trimEnd();
const total = num(f['GRAND TOTAL 2']);
- const item = String(f['DETAIL 1(1)'] || f['DETAIL 1'] || '').replace(/\s+$/, '').slice(0, 80);
+ const item = String(f['DETAIL 1(1)'] || f['DETAIL 1'] || '').trimEnd().slice(0, 80);
const rep = f['SALES PERSON'] ? ` · ${f['SALES PERSON']}` : '';
// Booked sale = payment received; otherwise it's an open quote (don't call it a sale).
const paid = String(f['PAID ON ACCOUNT'] ?? '').trim();
@@ -87,7 +75,7 @@ async function main() {
const tag = !booked ? '📝 *New quote* (no payment yet)' : (total < 0 ? '↩️ *Refund*' : '🛎️ *New sale*');
const text = `${tag} — ${client} · Invoice #${n} · *${money(total)}*${rep}${item ? `\n• ${item}` : ''}`;
if (DRY_RUN) console.log('[would post] ' + text.replace(/\n/g, ' '));
- else { await post(text); }
+ else await post(text);
if (n > maxPosted) maxPosted = n;
}
if (!DRY_RUN) writeFileSync(CURSOR, String(maxPosted));
diff --git a/scripts/sales-summary.mjs b/scripts/sales-summary.mjs
index eb7e1be..fa92a33 100644
--- a/scripts/sales-summary.mjs
+++ b/scripts/sales-summary.mjs
@@ -5,21 +5,11 @@
// node scripts/sales-summary.mjs # compute + POST to Slack
// node scripts/sales-summary.mjs --dry-run # compute + print only (no post)
-import { readFileSync } from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import { dirname, join } from 'node:path';
import { findRecords } from '../src/fm-client.js';
+import { loadEnv, TZ, ptYMD, num, money } from '../lib/fm-script-helpers.js';
-// --- load .env into process.env (dotenv isn't installed here) ---
-const __dir = dirname(fileURLToPath(import.meta.url));
-try {
- for (const line of readFileSync(join(__dir, '..', '.env'), 'utf8').split('\n')) {
- const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
- if (m && !(m[1] in process.env)) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
- }
-} catch { /* env may already be exported (e.g. launchd) */ }
+loadEnv(import.meta.url);
-const TZ = 'America/Los_Angeles';
const DB = 'invoice';
const LAYOUT = 'GRAND TOTAL SALES';
const TOTAL_FIELD = 'GRAND TOTAL 2'; // per-invoice grand total (merch + shipping + tax)
@@ -29,13 +19,6 @@ const DRY_RUN = process.argv.includes('--dry-run');
const EXCLUDE_INVOICES = new Set(['999999']); // test/demo records
const pad = (n) => String(n).padStart(2, '0');
-const num = (v) => { const n = parseFloat(v); return Number.isFinite(n) ? n : 0; };
-
-function ptYMD(d) {
- const p = new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(d);
- const g = (t) => p.find((x) => x.type === t).value;
- return { y: +g('year'), m: +g('month'), d: +g('day') };
-}
async function main() {
const now = new Date();
@@ -53,7 +36,10 @@ async function main() {
// A BOOKED order (= a real sale) has money received (PAID ON ACCOUNT ≠ 0).
// No payment yet = an open QUOTE, which must NOT count as sales.
- const isBooked = (f) => { const p = String(f['PAID ON ACCOUNT'] ?? '').trim(); return p !== '' && num(p) !== 0; };
+ const isBooked = (f) => {
+ const p = String(f['PAID ON ACCOUNT'] ?? '').trim();
+ return p !== '' && num(p) !== 0;
+ };
let dayCount = 0, dayTotal = 0, mtdCount = 0, mtdTotal = 0, qCount = 0, qTotal = 0;
for (const r of records) {
@@ -64,12 +50,11 @@ async function main() {
if (isBooked(f)) {
mtdCount += 1; mtdTotal += amt;
if (isToday) { dayCount += 1; dayTotal += amt; }
- } else if (isToday) { // today's quotes with no money received yet
+ } else if (isToday) { // today's quotes with no money received yet
qCount += 1; qTotal += amt;
}
}
- const money = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const dayLabel = new Intl.DateTimeFormat('en-US', { timeZone: TZ, weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }).format(now);
const monthLabel = new Intl.DateTimeFormat('en-US', { timeZone: TZ, month: 'long', year: 'numeric' }).format(now);
← fec4102 add DWTA baxter dedup: archive+delete script for FM dup reco
·
back to Filemaker Mcp
·
daily sales summary: move fire time 23:55 -> 16:30 (4:30 PM 70f934b →