@ -3,9 +3,10 @@
// and must_change_password=1 (they change it on first login). Active per ACTIVE_STAT.
// Idempotent: skips a username/email that already exists.
// DATABASE_URL=postgres://… npx tsx apps/hq/scripts/import-employees.ts
import { openDb } from '../src/db'
import { openPgDb } from '../src/db-pg'
import { openDb , sqliteFilePath } from '../src/db'
import { openPgDb , pgTargetDescription , requireDbUrlForEnv } from '../src/db-pg'
import { createEmployee } from '../src/repos-employees'
import type { DB } from '../src/db'
// From empmaster.xlsx (EMPNAME + ACTIVE_STAT + DESIGNATION code). Emails/phones were blank.
const EMPLOYEES : { name : string ; active : boolean ; title? : string } [ ] = [
@ -28,31 +29,42 @@ function slug(name: string): string {
}
async function main() {
// Same engine selection as the server: DATABASE_URL → Postgres, else SQLite at HQ_DATA_DIR.
const pgUrl = process . env [ 'DATABASE_URL' ] ? ? ''
const db = pgUrl !== '' ? await openPgDb ( pgUrl ) : openDb ( process . env [ 'HQ_DATA_DIR' ] )
let added = 0 , skipped = 0
const summary : string [ ] = [ ]
for ( const e of EMPLOYEES ) {
const username = slug ( e . name )
const password = ` ${ username } @123 `
const email = ` ${ username } @sims.local ` // placeholder (no real emails in the master)
try {
const emp = await createEmployee ( db , 'system' , {
email , username , displayName : e.name , role : 'staff' , password ,
mustChangePassword : true , . . . ( e . title !== undefined ? { title : e.title } : { } ) ,
} )
// Set inactive employees inactive (createEmployee makes them active by default).
if ( ! e . active ) await db . run ( ` UPDATE staff_user SET active=0 WHERE id=? ` , emp . id )
added ++
summary . push ( ` + ${ e . name } → username: ${ username } password: ${ password } ${ e . active ? '' : '(inactive)' } ` )
} catch ( err ) {
skipped ++
summary . push ( ` - ${ e . name } skipped: ${ err instanceof Error ? err.message : String ( err ) } ` )
// Pre-ship audit FIX A: the SAME resolver server.ts uses (never re-derived locally) — and,
// unlike the server, this one-off script hard-fails rather than silently falling back to a
// throwaway SQLite file when NODE_ENV=production resolves no Postgres target.
const pgUrl = requireDbUrlForEnv ( )
if ( pgUrl !== '' ) {
console . log ( ` [db] engine: postgres ${ pgTargetDescription ( pgUrl ) } ` )
} else {
console . log ( ` [db] engine: sqlite at ${ sqliteFilePath ( process . env [ 'HQ_DATA_DIR' ] ) } ` )
}
const db : DB = pgUrl !== '' ? await openPgDb ( pgUrl ) : openDb ( process . env [ 'HQ_DATA_DIR' ] )
try {
let added = 0 , skipped = 0
const summary : string [ ] = [ ]
for ( const e of EMPLOYEES ) {
const username = slug ( e . name )
const password = ` ${ username } @123 `
const email = ` ${ username } @sims.local ` // placeholder (no real emails in the master)
try {
const emp = await createEmployee ( db , 'system' , {
email , username , displayName : e.name , role : 'staff' , password ,
mustChangePassword : true , . . . ( e . title !== undefined ? { title : e.title } : { } ) ,
} )
// Set inactive employees inactive (createEmployee makes them active by default).
if ( ! e . active ) await db . run ( ` UPDATE staff_user SET active=0 WHERE id=? ` , emp . id )
added ++
summary . push ( ` + ${ e . name } → username: ${ username } password: ${ password } ${ e . active ? '' : '(inactive)' } ` )
} catch ( err ) {
skipped ++
summary . push ( ` - ${ e . name } skipped: ${ err instanceof Error ? err.message : String ( err ) } ` )
}
}
// eslint-disable-next-line no-console
console . log ( ` Employee import: ${ added } added, ${ skipped } skipped. \ n ${ summary . join ( '\n' ) } ` )
} finally {
await db . close ( )
}
// eslint-disable-next-line no-console
console . log ( ` Employee import: ${ added } added, ${ skipped } skipped. \ n ${ summary . join ( '\n' ) } ` )
}
main ( ) . catch ( ( e ) = > { console . error ( e ) ; process . exit ( 1 ) } )