formlab-mcp 0.6.54 → 0.6.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.json +2 -2
- package/tools/analytics.js +11 -1
- package/vendor/cost.js +39 -11
- package/vendor/equipment-calibration.js +51 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "formlab-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.56",
|
|
4
4
|
"mcpName": "io.github.juliu1980/formlab-mcp",
|
|
5
5
|
"description": "Read-only Model Context Protocol server for FormLab \u2014 lets Claude (and other MCP clients) read and analyze your FormLab data, from a local export file OR your live cloud workspace.",
|
|
6
6
|
"type": "module",
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.juliu1980/formlab-mcp",
|
|
4
4
|
"description": "Read-only MCP for FormLab — let Claude query your formulation lab. Free reads a local export; Pro connects to your live cloud workspace with a dedicated read-only token.",
|
|
5
|
-
"version": "0.6.
|
|
5
|
+
"version": "0.6.56",
|
|
6
6
|
"websiteUrl": "https://formvix.com/mcp",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/juliu1980/FormLab",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "formlab-mcp",
|
|
16
|
-
"version": "0.6.
|
|
16
|
+
"version": "0.6.56",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
package/tools/analytics.js
CHANGED
|
@@ -1279,6 +1279,16 @@ function _iChart(ys) {
|
|
|
1279
1279
|
|
|
1280
1280
|
// Shelf life: fit vs elapsed months, extrapolate to the spec limit the trend
|
|
1281
1281
|
// heads toward. Point-estimate crossing + the conservative 95% CI-bound crossing.
|
|
1282
|
+
// One-sided 95% Student t for df = n − 2 (ICH Q1E) — a copy of the app's _stabT95
|
|
1283
|
+
// (js/views/stability.js); test/stability-ich-t.test.js holds the two equal.
|
|
1284
|
+
const _T95_TABLE = [null, 6.314, 2.920, 2.353, 2.132, 2.015, 1.943, 1.895, 1.860, 1.833, 1.812, 1.796, 1.782, 1.771, 1.761, 1.753, 1.746, 1.740, 1.734, 1.729, 1.725, 1.721, 1.717, 1.714, 1.711, 1.708, 1.706, 1.703, 1.701, 1.699, 1.697];
|
|
1285
|
+
function _t95(df) {
|
|
1286
|
+
df = Math.floor(df);
|
|
1287
|
+
if (!(df >= 1)) return Infinity;
|
|
1288
|
+
if (df < _T95_TABLE.length) return _T95_TABLE[df];
|
|
1289
|
+
const z = 1.6448536;
|
|
1290
|
+
return z + (z ** 3 + z) / (4 * df) + (5 * z ** 5 + 16 * z ** 3 + 3 * z) / (96 * df * df);
|
|
1291
|
+
}
|
|
1282
1292
|
function _shelfLife(points, spec) {
|
|
1283
1293
|
if (!points || points.length < 3 || (spec.min == null && spec.max == null)) return null;
|
|
1284
1294
|
const t0 = Math.min(...points.map(p => p.date.getTime()));
|
|
@@ -1295,7 +1305,7 @@ function _shelfLife(points, spec) {
|
|
|
1295
1305
|
const ssX = xs.reduce((a, b) => a + (b - mx) ** 2, 0);
|
|
1296
1306
|
let ssR = 0; xs.forEach((x, i) => { const pr = fit.intercept + fit.slope * x; ssR += (ys[i] - pr) ** 2; });
|
|
1297
1307
|
const sResid = Math.sqrt(ssR / Math.max(1, n - 2));
|
|
1298
|
-
const tcrit = n
|
|
1308
|
+
const tcrit = _t95(n - 2); // ICH Q1E one-sided 95% (app 22t, js/views/stability.js _stabT95)
|
|
1299
1309
|
const sign = fit.slope < 0 ? -1 : 1;
|
|
1300
1310
|
const bound = (tt) => fit.intercept + fit.slope * tt + sign * tcrit * sResid * Math.sqrt(1 / n + ((tt - mx) ** 2) / Math.max(1e-9, ssX));
|
|
1301
1311
|
let ciRemaining = null;
|
package/vendor/cost.js
CHANGED
|
@@ -138,28 +138,39 @@ function getAllUnits() {
|
|
|
138
138
|
const custom = Object.keys((typeof db !== 'undefined' && db?.customUnits) || {});
|
|
139
139
|
return [...new Set([...builtin, ...custom])];
|
|
140
140
|
}
|
|
141
|
+
// The units people reach for first, in that order (22x). Every list puts these ahead of the
|
|
142
|
+
// rest of their group (the picker groups by Mass / Volume / …), so a list opens on g, kg, mg —
|
|
143
|
+
// not on µg, ug, mcg, the size order the conversion tables are written in. Everything else,
|
|
144
|
+
// custom units included, keeps its place after them.
|
|
145
|
+
const _UNITS_COMMON_FIRST = ['wt%', 'vol%', 'g', 'kg', 'mg', 'µg', 'lb', 'oz', 't', 'tonne', 'ton',
|
|
146
|
+
'mL', 'L', 'µL', 'gal', 'qt', 'pt', 'fl_oz', 'm³', 'mm', 'cm', 'm', 'in', 'ft', 'units', 'each'];
|
|
147
|
+
function unitsCommonFirst(units) {
|
|
148
|
+
const rank = new Map(_UNITS_COMMON_FIRST.map((u, i) => [u, i]));
|
|
149
|
+
return units.map((u, i) => [u, rank.has(u) ? rank.get(u) : 1000 + i]).sort((a, b) => a[1] - b[1]).map(x => x[0]);
|
|
150
|
+
}
|
|
141
151
|
function ensureUnitDatalists() {
|
|
142
152
|
if (typeof document === 'undefined' || !document.body) return;
|
|
143
153
|
const q = (s) => String(s).replace(/"/g, '"');
|
|
144
154
|
const make = (id, units) => {
|
|
145
155
|
let dl = document.getElementById(id);
|
|
146
156
|
if (!dl) { dl = document.createElement('datalist'); dl.id = id; document.body.appendChild(dl); }
|
|
147
|
-
dl.innerHTML = units.map((u) => `<option value="${q(u)}"></option>`).join('');
|
|
157
|
+
dl.innerHTML = unitsCommonFirst(units).map((u) => `<option value="${q(u)}"></option>`).join('');
|
|
148
158
|
};
|
|
149
159
|
make('fl-units-all', getAllUnits()); // composition / batch quantity
|
|
150
160
|
make('fl-units-cost', getCostUnits()); // ingredient set-price (mass/vol/length/each)
|
|
151
|
-
make('fl-units-qty', getQuantityUnits()); //
|
|
161
|
+
make('fl-units-qty', getQuantityUnits()); // an amount made: batch target, sample amount (mass/vol/count)
|
|
162
|
+
make('fl-units-vol', Object.keys(getVolUnits())); // a measured volume (22w)
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
// Units for a
|
|
155
|
-
//
|
|
156
|
-
//
|
|
165
|
+
// Units for an amount made — a batch's target quantity and a sample's amount (22w: one list
|
|
166
|
+
// for both; the batch had borrowed the composition list, wt% / ppm / lengths / mol included,
|
|
167
|
+
// while samples lacked tonne, bbl, m³…): every mass and volume unit (your custom ones
|
|
168
|
+
// included), plus a count. NOT fractions (wt% / ppm), which only make sense in a recipe ratio,
|
|
169
|
+
// and not lengths.
|
|
157
170
|
function getQuantityUnits() {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const custom = Object.keys((typeof db !== 'undefined' && db?.customUnits) || {});
|
|
162
|
-
return [...new Set([...mass, ...vol, ...count, ...custom])];
|
|
171
|
+
const customCount = Object.entries((typeof db !== 'undefined' && db?.customUnits) || {})
|
|
172
|
+
.filter(([_, v]) => v?.dimension === 'count').map(([k]) => k);
|
|
173
|
+
return [...new Set([...Object.keys(getMassUnits()), ...Object.keys(getVolUnits()), 'units', 'each', ...customCount])];
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
// ============================================================
|
|
@@ -309,9 +320,25 @@ function getQuantityUnits() {
|
|
|
309
320
|
}
|
|
310
321
|
|
|
311
322
|
document.addEventListener('focusin', function (e) { if (isCombo(e.target) && !refocusing) open(e.target); });
|
|
323
|
+
// The ▾ drawn at the right end (css input.fl-unit-combo) acts like a ▾ button, as
|
|
324
|
+
// js/combo.js's fields do (22v): a hand over it, and a click there closes an OPEN list.
|
|
325
|
+
// "Open" is judged at mousedown: a click into an unfocused field opens it on focusin, and
|
|
326
|
+
// that same click must not close it again.
|
|
327
|
+
var ARROW_W = 28, openAtDown = false;
|
|
328
|
+
function onArrow(el, e) { var r = el.getBoundingClientRect(); return e.clientX >= r.right - ARROW_W; }
|
|
329
|
+
document.addEventListener('mousemove', function (e) {
|
|
330
|
+
if (!isCombo(e.target)) return;
|
|
331
|
+
var c = onArrow(e.target, e) ? 'pointer' : '';
|
|
332
|
+
if (e.target.style.cursor !== c) e.target.style.cursor = c;
|
|
333
|
+
});
|
|
334
|
+
document.addEventListener('mouseout', function (e) { if (isCombo(e.target)) e.target.style.cursor = ''; });
|
|
312
335
|
// A click on the field reopens the list after a pick (the field kept focus,
|
|
313
336
|
// so no focusin fires). Capture phase: dialogs stop click propagation.
|
|
314
|
-
document.addEventListener('click', function (e) {
|
|
337
|
+
document.addEventListener('click', function (e) {
|
|
338
|
+
if (!isCombo(e.target)) return;
|
|
339
|
+
if (openAtDown && onArrow(e.target, e)) { openAtDown = false; hide(); return; }
|
|
340
|
+
if (!isOpen()) open(e.target);
|
|
341
|
+
}, true);
|
|
315
342
|
// Typing directly in the field filters too (only matters when there's no
|
|
316
343
|
// search box — i.e. short lists; otherwise focus is in the search box).
|
|
317
344
|
document.addEventListener('input', function (e) {
|
|
@@ -322,6 +349,7 @@ function getQuantityUnits() {
|
|
|
322
349
|
document.addEventListener('keydown', function (e) { if (isCombo(e.target)) navKeydown(e); });
|
|
323
350
|
// mousedown (capture) so a click on a suggestion wins over the input's blur.
|
|
324
351
|
document.addEventListener('mousedown', function (e) {
|
|
352
|
+
if (isCombo(e.target)) openAtDown = isOpen() && activeInput === e.target;
|
|
325
353
|
if (!isOpen()) return;
|
|
326
354
|
var item = e.target.closest && e.target.closest('.fl-unit-combo-item');
|
|
327
355
|
if (item) { e.preventDefault(); choose(item.getAttribute('data-val')); return; }
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
// ============================================================
|
|
17
17
|
|
|
18
18
|
const EQ_CAL_SOON_DAYS = 30;
|
|
19
|
+
// The longest interval taken as real (20 years). Anything above, at or below zero, or not a
|
|
20
|
+
// number counts as "no interval": a typo like 1e9 months used to throw in the date maths
|
|
21
|
+
// and take the Equipment list and the dashboard down with it.
|
|
22
|
+
const EQ_CAL_MAX_MONTHS = 240;
|
|
19
23
|
const EQ_CAL_RESULTS = [
|
|
20
24
|
{ value: 'pass', label: 'Pass', hint: 'within tolerance as found' },
|
|
21
25
|
{ value: 'adjusted', label: 'Adjusted, then pass', hint: 'out of tolerance as found, adjusted and verified' },
|
|
@@ -23,20 +27,38 @@ const EQ_CAL_RESULTS = [
|
|
|
23
27
|
];
|
|
24
28
|
const _EQ_CAL_RESULT_LABEL = Object.fromEntries(EQ_CAL_RESULTS.map(r => [r.value, r.label]));
|
|
25
29
|
|
|
30
|
+
// Today in LOCAL time (flTodayIso when utils.js is loaded; the MCP copy passes its own).
|
|
31
|
+
function _eqCalToday() {
|
|
32
|
+
if (typeof flTodayIso === 'function') return flTodayIso();
|
|
33
|
+
const d = new Date();
|
|
34
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
35
|
+
}
|
|
26
36
|
function eqCalOn() { return typeof hasFeature !== 'function' || hasFeature('equipment.calibration'); }
|
|
37
|
+
// Whole months, 1..EQ_CAL_MAX_MONTHS, else 0.
|
|
38
|
+
function eqCalInterval(e) {
|
|
39
|
+
const n = Math.round(Number(e && e.calIntervalMonths));
|
|
40
|
+
return (n >= 1 && n <= EQ_CAL_MAX_MONTHS) ? n : 0;
|
|
41
|
+
}
|
|
42
|
+
const _EQ_CAL_ISO = /^\d{4}-\d{2}-\d{2}$/;
|
|
43
|
+
// Ids go into onclick="fn('…')": escJsAttr (utils.js) also covers the ' that esc() leaves.
|
|
44
|
+
function _eqCalJs(v) { return typeof escJsAttr === 'function' ? escJsAttr(v) : String(v == null ? '' : v).replace(/[^\w-]/g, ''); }
|
|
27
45
|
|
|
28
|
-
// Newest first; entries without a date last.
|
|
46
|
+
// Newest first; entries without a date last. Two on the same day (a fail, then the
|
|
47
|
+
// adjusted re-calibration) are ordered by when they were logged, the later first.
|
|
29
48
|
function eqCalHistory(e) {
|
|
30
|
-
const list = (e && Array.isArray(e.calibrations)) ? e.calibrations.filter(
|
|
31
|
-
return list.slice().sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))
|
|
49
|
+
const list = (e && Array.isArray(e.calibrations)) ? e.calibrations.filter(c => c && typeof c === 'object') : [];
|
|
50
|
+
return list.slice().sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))
|
|
51
|
+
|| String(b.loggedAt || '').localeCompare(String(a.loggedAt || '')));
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
// Calendar months, clamped to the month's last day (Jan 31 + 1 mo = Feb 28/29), as
|
|
35
55
|
// flDateAddIso (utils.js) — kept here so the MCP server's vendored copy needs nothing else.
|
|
36
56
|
function _eqCalAddMonths(iso, months) {
|
|
37
|
-
|
|
57
|
+
months = Math.round(Number(months));
|
|
58
|
+
if (!_EQ_CAL_ISO.test(String(iso || '')) || !(months > 0) || months > EQ_CAL_MAX_MONTHS) return '';
|
|
38
59
|
const [y, m, d] = String(iso).split('-').map(Number);
|
|
39
60
|
const first = new Date(Date.UTC(y, m - 1 + months, 1));
|
|
61
|
+
if (!isFinite(first.getTime())) return '';
|
|
40
62
|
const last = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate();
|
|
41
63
|
return new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth(), Math.min(d, last))).toISOString().slice(0, 10);
|
|
42
64
|
}
|
|
@@ -54,16 +76,19 @@ function _eqCalDays(fromIso, toIso) {
|
|
|
54
76
|
// 'ok'
|
|
55
77
|
// due: the next due date (the latest entry's own nextDue, else its date + the interval)
|
|
56
78
|
function eqCalStatus(e, todayIso) {
|
|
57
|
-
const today = todayIso || (
|
|
58
|
-
const interval =
|
|
79
|
+
const today = todayIso || _eqCalToday();
|
|
80
|
+
const interval = eqCalInterval(e);
|
|
59
81
|
const hist = eqCalHistory(e);
|
|
60
82
|
const last = hist[0] || null;
|
|
61
|
-
|
|
83
|
+
// Results are written lower-case; an imported "FAIL" is still a fail.
|
|
84
|
+
const result = last ? String(last.result || '').trim().toLowerCase() : '';
|
|
85
|
+
const base = { state: 'none', due: '', last: last ? (last.date || '') : '', lastResult: result, interval, daysToDue: null, count: hist.length };
|
|
62
86
|
if (!e || e.status === 'Retired') return base;
|
|
63
87
|
if (!last) return interval ? { ...base, state: 'never' } : base;
|
|
64
88
|
// A failed instrument has no next due date: it needs a passing calibration first.
|
|
65
|
-
if (
|
|
66
|
-
|
|
89
|
+
if (result === 'fail') return { ...base, state: 'failed' };
|
|
90
|
+
// A next due that is not a date (an import's "soon") falls back to the interval.
|
|
91
|
+
const due = (_EQ_CAL_ISO.test(String(last.nextDue || '')) ? last.nextDue : '') || _eqCalAddMonths(last.date, interval) || '';
|
|
67
92
|
const days = due ? _eqCalDays(today, due) : null;
|
|
68
93
|
if (!due) return { ...base, state: 'ok' };
|
|
69
94
|
const state = days < 0 ? 'overdue' : days <= EQ_CAL_SOON_DAYS ? 'soon' : 'ok';
|
|
@@ -112,12 +137,12 @@ function eqCalCardHtml(e) {
|
|
|
112
137
|
</div></div>`;
|
|
113
138
|
}
|
|
114
139
|
const st = eqCalStatus(e);
|
|
115
|
-
const interval =
|
|
140
|
+
const interval = eqCalInterval(e);
|
|
116
141
|
const files = Array.isArray(e.fileAttachments) ? e.fileAttachments : [];
|
|
117
142
|
const certName = (id) => { const f = files.find(a => a && a.id === id); return f ? (f.name || f.title || f.url || 'Certificate') : ''; };
|
|
118
143
|
const editBtns = on ? `<div style="display:flex;gap:6px">
|
|
119
|
-
<button class="btn btn-secondary btn-sm" onclick="openEqCalIntervalModal('${e.id}')">${interval ? 'Every ' + interval + ' mo' : 'Set interval'}</button>
|
|
120
|
-
<button class="btn btn-primary btn-sm" onclick="openEqCalLogModal('${e.id}')">+ Log calibration</button>
|
|
144
|
+
<button class="btn btn-secondary btn-sm" onclick="openEqCalIntervalModal('${_eqCalJs(e.id)}')">${interval ? 'Every ' + interval + ' mo' : 'Set interval'}</button>
|
|
145
|
+
<button class="btn btn-primary btn-sm" onclick="openEqCalLogModal('${_eqCalJs(e.id)}')">+ Log calibration</button>
|
|
121
146
|
</div>` : `<button class="btn btn-secondary btn-sm" onclick="proUpsell('equipment.calibration')"><span class="fl-pro" aria-hidden="true"></span> Pro</button>`;
|
|
122
147
|
const summary = st.state === 'none'
|
|
123
148
|
? `<div style="color:var(--text-muted);font-size:13px">Not tracked yet. Set how often it is calibrated, or log its last calibration.</div>`
|
|
@@ -132,9 +157,9 @@ function eqCalCardHtml(e) {
|
|
|
132
157
|
<td>${esc(c.by || '—')}</td>
|
|
133
158
|
<td>${c.result ? `<span class="badge" style="${_EQ_CAL_TONE[c.result === 'fail' ? 'failed' : c.result === 'adjusted' ? 'soon' : 'ok']};font-size:11px">${esc(_EQ_CAL_RESULT_LABEL[c.result] || c.result)}</span>` : '—'}</td>
|
|
134
159
|
<td style="font-variant-numeric:tabular-nums">${esc(c.nextDue || '—')}</td>
|
|
135
|
-
<td>${c.certificateId && certName(c.certificateId) ? `<a style="color:var(--primary);cursor:pointer" onclick="attachOpenInTab('${
|
|
160
|
+
<td>${c.certificateId && certName(c.certificateId) ? `<a style="color:var(--primary);cursor:pointer" onclick="attachOpenInTab('${_eqCalJs(c.certificateId)}')">${esc(certName(c.certificateId))}</a>` : (c.certificateRef ? esc(c.certificateRef) : '<span style="color:var(--dk-dim, #cbd5e1)">—</span>')}</td>
|
|
136
161
|
<td style="color:var(--text-muted);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(c.notes || '')}">${esc(c.notes || '')}</td>
|
|
137
|
-
<td style="text-align:right;white-space:nowrap">${on ? `<button class="btn btn-secondary btn-sm" onclick="openEqCalLogModal('${e.id}','${
|
|
162
|
+
<td style="text-align:right;white-space:nowrap">${on ? `<button class="btn btn-secondary btn-sm" onclick="openEqCalLogModal('${_eqCalJs(e.id)}','${_eqCalJs(c.id)}')" title="Edit this calibration">${typeof ICON !== 'undefined' && ICON.edit ? ICON.edit : 'Edit'}</button>` : ''}</td>
|
|
138
163
|
</tr>`).join('');
|
|
139
164
|
const table = hist.length ? `<div class="table-wrap" style="margin-top:14px"><table class="eq-cal-table">
|
|
140
165
|
<thead><tr><th>Date</th><th>Performed by</th><th>Result</th><th>Next due</th><th>Certificate</th><th>Notes</th><th></th></tr></thead>
|
|
@@ -152,12 +177,13 @@ function openEqCalIntervalModal(eqId) {
|
|
|
152
177
|
if (!e) return;
|
|
153
178
|
showModal('Calibration interval', `
|
|
154
179
|
<div class="form-group"><label>Calibrate every</label>
|
|
155
|
-
<div style="display:flex;align-items:center;gap:8px"><input id="eq-cal-interval" type="number" min="0" step="1" style="width:110px" value="${esc(e
|
|
180
|
+
<div style="display:flex;align-items:center;gap:8px"><input id="eq-cal-interval" type="number" min="0" step="1" style="width:110px" value="${esc(eqCalInterval(e) || '')}" placeholder="12" max="${EQ_CAL_MAX_MONTHS}"> <span>months</span></div>
|
|
156
181
|
<div style="font-size:12px;color:var(--text-muted);margin-top:6px">Sets the next due date from each calibration's date. Leave empty to stop tracking a due date (the history stays).</div>
|
|
157
182
|
</div>`, () => {
|
|
158
183
|
const raw = (document.getElementById('eq-cal-interval') || {}).value;
|
|
159
184
|
const n = raw === '' ? null : Math.round(Number(raw));
|
|
160
185
|
if (n != null && !(n >= 0)) { toast('Enter a number of months.'); return false; }
|
|
186
|
+
if (n > EQ_CAL_MAX_MONTHS) { toast(`Up to ${EQ_CAL_MAX_MONTHS} months (20 years).`); return false; }
|
|
161
187
|
updateEquipment(eqId, { calIntervalMonths: n || null });
|
|
162
188
|
if (typeof renderEquipmentDetail === 'function') renderEquipmentDetail(eqId);
|
|
163
189
|
return true;
|
|
@@ -173,14 +199,14 @@ function openEqCalLogModal(eqId, calId) {
|
|
|
173
199
|
const e = typeof getEquipment === 'function' ? getEquipment(eqId) : (db.equipment || []).find(x => x.id === eqId);
|
|
174
200
|
if (!e) return;
|
|
175
201
|
const c = calId ? (e.calibrations || []).find(x => x && x.id === calId) : null;
|
|
176
|
-
const interval =
|
|
177
|
-
const date = c ? (c.date || '') : (
|
|
202
|
+
const interval = eqCalInterval(e);
|
|
203
|
+
const date = c ? (c.date || '') : _eqCalToday();
|
|
178
204
|
const files = Array.isArray(e.fileAttachments) ? e.fileAttachments : [];
|
|
179
205
|
const certOpts = `<option value="">— None —</option>` + files.map(a => `<option value="${esc(a.id)}"${c && c.certificateId === a.id ? ' selected' : ''}>${esc(a.name || a.title || a.url || a.id)}</option>`).join('');
|
|
180
206
|
const hint = (t) => `<div style="font-size:11.5px;color:var(--text-muted);margin-top:4px;line-height:1.4">${t}</div>`;
|
|
181
207
|
const body = `<div class="form-grid">
|
|
182
|
-
<div class="form-group"><label>Date calibrated *</label><input id="eq-cal-date" type="date" value="${esc(date)}" onchange="_eqCalAutoDue(${interval})"></div>
|
|
183
|
-
<div class="form-group"><label>Performed by</label><input id="eq-cal-by" value="${esc(c ? (c.by || '') : _eqCalMe())}" placeholder="Name, or the service provider"></div>
|
|
208
|
+
<div class="form-group"><label>Date calibrated *</label><input id="eq-cal-date" type="date" value="${esc(date)}" max="${_eqCalToday()}" onchange="_eqCalAutoDue(${interval})"></div>
|
|
209
|
+
<div class="form-group"><label>Performed by</label><input id="eq-cal-by" value="${esc(c ? (c.by || '') : _eqCalMe())}" placeholder="Name, or the service provider" data-fl-person="equipment.calibrations[].by testResults.performedBy equipment.owner"></div>
|
|
184
210
|
<div class="form-group span-2"><label>Result *</label>
|
|
185
211
|
<select id="eq-cal-result">${EQ_CAL_RESULTS.map(r => `<option value="${r.value}"${(c ? c.result : 'pass') === r.value ? ' selected' : ''}>${esc(r.label)} — ${esc(r.hint)}</option>`).join('')}</select>
|
|
186
212
|
</div>
|
|
@@ -205,9 +231,13 @@ function openEqCalLogModal(eqId, calId) {
|
|
|
205
231
|
loggedBy: c ? (c.loggedBy || '') : _eqCalMe(),
|
|
206
232
|
};
|
|
207
233
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(rec.date)) { toast('Enter the date it was calibrated.'); return false; }
|
|
234
|
+
if (rec.date > _eqCalToday()) { toast('The calibration date is in the future. Log it once it is done.'); return false; }
|
|
208
235
|
if (rec.nextDue && rec.nextDue < rec.date) { toast('Next due is before the calibration date.'); return false; }
|
|
209
236
|
Object.keys(rec).forEach(k => rec[k] === undefined && delete rec[k]);
|
|
210
|
-
|
|
237
|
+
// The record as it is NOW: a calibration synced in from another device while this
|
|
238
|
+
// dialog was open must not be dropped by a list read when it opened.
|
|
239
|
+
const cur = (typeof getEquipment === 'function' ? getEquipment(eqId) : null) || e;
|
|
240
|
+
const list = (cur.calibrations || []).filter(x => x && x.id !== rec.id);
|
|
211
241
|
list.push(rec);
|
|
212
242
|
const patch = { calibrations: list };
|
|
213
243
|
updateEquipment(eqId, patch);
|
|
@@ -219,7 +249,7 @@ function openEqCalLogModal(eqId, calId) {
|
|
|
219
249
|
// Delete sits in the dialog's footer, left of the buttons.
|
|
220
250
|
setTimeout(() => {
|
|
221
251
|
const foot = document.querySelector('#modal-backdrop .modal-footer');
|
|
222
|
-
if (foot && !foot.querySelector('.eq-cal-del')) foot.insertAdjacentHTML('afterbegin', `<button class="btn btn-danger btn-sm eq-cal-del" style="margin-right:auto" onclick="deleteEqCal('${eqId}','${
|
|
252
|
+
if (foot && !foot.querySelector('.eq-cal-del')) foot.insertAdjacentHTML('afterbegin', `<button class="btn btn-danger btn-sm eq-cal-del" style="margin-right:auto" onclick="deleteEqCal('${_eqCalJs(eqId)}','${_eqCalJs(c.id)}')">Delete</button>`);
|
|
223
253
|
}, 0);
|
|
224
254
|
}
|
|
225
255
|
}
|