initial commit
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/// <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);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user