165 lines
5.2 KiB
JavaScript
165 lines
5.2 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
//
|
|
// M8 — Align appointments with the frontend's structured field model.
|
|
//
|
|
// Older databases used these fields:
|
|
// appointmentDateTime, duration, type
|
|
// and often embedded VIN/duration inside notes.
|
|
//
|
|
// The current frontend expects first-class fields:
|
|
// date, time, vin, durationMinutes, advisorName, serviceType
|
|
//
|
|
// This migration adds any missing fields and backfills them from the legacy
|
|
// columns while preserving existing data.
|
|
//
|
|
|
|
function ensureTextField(col, name) {
|
|
if (!col.fields.find((f) => f.name === name)) {
|
|
col.fields.push(new TextField({ name, required: false }));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function ensureNumberField(col, name) {
|
|
if (!col.fields.find((f) => f.name === name)) {
|
|
col.fields.push(new NumberField({ name, required: false, min: 0 }));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function splitAppointmentDateTime(value) {
|
|
const raw = String(value || '').trim();
|
|
const match = raw.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/);
|
|
if (match) {
|
|
return { date: match[1], time: match[2] };
|
|
}
|
|
return { date: '', time: '' };
|
|
}
|
|
|
|
function extractVinFromNotes(notes) {
|
|
const match = String(notes || '').match(/(?:^|\|)\s*VIN:\s*([^|]+?)\s*(?=\||$)/i);
|
|
return match?.[1]?.trim().toUpperCase() || '';
|
|
}
|
|
|
|
function extractDurationFromNotes(notes) {
|
|
const match = String(notes || '').match(/(?:^|\|)\s*Duration:\s*(\d+)\s*min\s*(?=\||$)/i);
|
|
return match?.[1] ? Number(match[1]) : null;
|
|
}
|
|
|
|
function stripStructuredAppointmentNotes(notes) {
|
|
return String(notes || '')
|
|
.split('|')
|
|
.map((part) => part.trim())
|
|
.filter((part) => part && !/^VIN:\s*/i.test(part) && !/^Duration:\s*\d+\s*min$/i.test(part))
|
|
.join(' | ');
|
|
}
|
|
|
|
migrate(
|
|
(db) => {
|
|
const col = db.findCollectionByNameOrId('appointments');
|
|
if (!col) {
|
|
console.warn('[M8] appointments collection not found — skipping');
|
|
return;
|
|
}
|
|
|
|
let schemaChanged = false;
|
|
schemaChanged = ensureTextField(col, 'date') || schemaChanged;
|
|
schemaChanged = ensureTextField(col, 'time') || schemaChanged;
|
|
schemaChanged = ensureTextField(col, 'vin') || schemaChanged;
|
|
schemaChanged = ensureTextField(col, 'advisorName') || schemaChanged;
|
|
schemaChanged = ensureTextField(col, 'serviceType') || schemaChanged;
|
|
schemaChanged = ensureNumberField(col, 'durationMinutes') || schemaChanged;
|
|
|
|
if (schemaChanged) {
|
|
db.save(col);
|
|
console.log('[M8] aligned appointments schema');
|
|
}
|
|
|
|
const records = Array.from(db.findRecordsByFilter('appointments', '1=1', 'id', 1000));
|
|
let backfilled = 0;
|
|
|
|
for (const rec of records) {
|
|
const notes = String(rec.get('notes') || '');
|
|
const legacyDateTime = String(rec.get('appointmentDateTime') || '');
|
|
const split = splitAppointmentDateTime(legacyDateTime);
|
|
const currentDate = String(rec.get('date') || '').trim();
|
|
const currentTime = String(rec.get('time') || '').trim();
|
|
const currentVin = String(rec.get('vin') || '').trim();
|
|
const currentServiceType = String(rec.get('serviceType') || '').trim();
|
|
const rawDurationMinutes = rec.get('durationMinutes');
|
|
const currentDurationMinutes = rawDurationMinutes == null || rawDurationMinutes === ''
|
|
? null
|
|
: Number(rawDurationMinutes);
|
|
const legacyDuration = rec.get('duration');
|
|
const numericLegacyDuration = legacyDuration == null || legacyDuration === ''
|
|
? null
|
|
: Number(legacyDuration);
|
|
|
|
let changed = false;
|
|
|
|
if (!currentDate && split.date) {
|
|
rec.set('date', split.date);
|
|
changed = true;
|
|
}
|
|
|
|
if (!currentTime && split.time) {
|
|
rec.set('time', split.time);
|
|
changed = true;
|
|
}
|
|
|
|
if (!currentVin) {
|
|
const noteVin = extractVinFromNotes(notes);
|
|
if (noteVin) {
|
|
rec.set('vin', noteVin);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (!currentServiceType) {
|
|
const legacyType = String(rec.get('type') || '').trim();
|
|
if (legacyType) {
|
|
rec.set('serviceType', legacyType);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (currentDurationMinutes == null || Number.isNaN(currentDurationMinutes) || (currentDurationMinutes <= 0 && numericLegacyDuration > 0)) {
|
|
const noteDuration = extractDurationFromNotes(notes);
|
|
const nextDuration = noteDuration ?? numericLegacyDuration;
|
|
if (nextDuration != null && !Number.isNaN(nextDuration)) {
|
|
rec.set('durationMinutes', nextDuration);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
const cleanedNotes = stripStructuredAppointmentNotes(notes);
|
|
if (cleanedNotes !== notes.trim()) {
|
|
rec.set('notes', cleanedNotes);
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) {
|
|
db.saveRecord(rec);
|
|
backfilled++;
|
|
}
|
|
}
|
|
|
|
console.log(`[M8] backfilled ${backfilled} appointment record(s)`);
|
|
},
|
|
(db) => {
|
|
const col = db.findCollectionByNameOrId('appointments');
|
|
if (!col) return;
|
|
|
|
for (const name of ['durationMinutes', 'serviceType', 'advisorName', 'vin', 'time', 'date']) {
|
|
const idx = col.fields.findIndex((f) => f.name === name);
|
|
if (idx >= 0) {
|
|
col.fields.splice(idx, 1);
|
|
}
|
|
}
|
|
|
|
db.save(col);
|
|
}
|
|
);
|