mega-plant/scripts/setup-clock.mjs

55 lines
3.5 KiB
JavaScript
Raw Normal View History

// Additive only. Never seeds, copies, deletes or updates employee/financial rows.
// node --env-file=.env.local scripts/setup-clock.mjs [--apply]
import PocketBase from 'pocketbase';
const apply=process.argv.includes('--apply');
try {
const pb=new PocketBase(process.env.NEXT_PUBLIC_PB_URL);
pb.autoCancellation(false);
await pb.collection('_superusers').authWithPassword(process.env.PB_ADMIN,process.env.PB_PASS);
const sites=await pb.collections.getOne('sites');
const users=await pb.collections.getOne('users');
const schemas=[
{name:'clock_events',fields:[
{name:'employee',type:'text',required:true},
{name:'employee_no',type:'text',required:true},{name:'employee_name',type:'text',required:true},
// Plant staff clock in at the Plant, not a site. Optional so older rows keep theirs.
{name:'site',type:'relation',collectionId:sites.id,required:false,maxSelect:1,cascadeDelete:false},
{name:'direction',type:'select',required:true,maxSelect:1,values:['in','out']},
{name:'scanned_at',type:'date',required:true},
{name:'actor',type:'relation',collectionId:users.id,required:true,maxSelect:1,cascadeDelete:false},
{name:'previous_event',type:'text'},{name:'request_signature',type:'text',required:true},
{name:'session_minutes',type:'number',min:0,onlyInt:true},
{name:'has_location',type:'bool'}, {name:'latitude',type:'number',min:-90,max:90},
{name:'longitude',type:'number',min:-180,max:180},{name:'location_accuracy_m',type:'number',min:0},
], indexes:[
'CREATE INDEX idx_plant_clock_employee_time ON clock_events (employee, scanned_at DESC)',
'CREATE UNIQUE INDEX idx_plant_clock_previous ON clock_events (employee, previous_event)',
]},
{name:'clock_scan_locks',fields:[{name:'employee',type:'text',required:true},{name:'created',type:'autodate',onCreate:true,onUpdate:false}],
indexes:['CREATE UNIQUE INDEX idx_plant_clock_lock ON clock_scan_locks (employee)']},
];
for(const schema of schemas) {
let existing;
try {existing=await pb.collections.getOne(schema.name);}catch(e){if(e.status!==404)throw e;}
if(existing) {
const missing=schema.fields.filter(f=>!existing.fields.some(h=>h.name===f.name && h.type===f.type));
if(missing.length || schema.indexes.some(i=>!existing.indexes.includes(i)) || ['listRule','viewRule','createRule','updateRule','deleteRule'].some(r=>existing[r]!==null)) {
throw new Error(`${schema.name} schema/rules differ from the clock contract; review before changing existing data.`);
}
// Relax-only migration: the first build required a scanning site.
const site=existing.fields.find(f=>f.name==='site');
if(schema.name==='clock_events' && site?.required) {
if(!apply){console.log('clock_events.site: would make optional (Plant scans have no site)');continue;}
await pb.collections.update(existing.id,{fields:existing.fields.map(f=>f.name==='site'?{...f,required:false}:f)});
console.log('clock_events.site: now optional');continue;
}
console.log(`${schema.name}: already ready`);continue;
}
if(!apply){console.log(`${schema.name}: would create private collection`);continue;}
await pb.send('/api/collections',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
type:'base',...schema,listRule:null,viewRule:null,createRule:null,updateRule:null,deleteRule:null,
})});
console.log(`${schema.name}: created (server access only)`);
}
} catch(e) {console.error('Clock setup failed:',e.status??'',e.message);process.exitCode=1;}