16ad0955f2
Deploy to route.crispygoat.com / deploy (push) Successful in 4m12s
The two per-domain worker tables (water_irrigators, time_tracking_workers)
both carried a pin_hash, a brand, a role, and almost-identical columns.
Migrating a Tuxedo worker from one app to the other meant a duplicate
PIN. Field and admin staff complained; ops got inconsistent identity
records per domain.
Unify into a single field_workers table. Cycle 10 migration
0097_field_workers.sql:
- creates field_workers (id, brand_id, name, pin_hash, role with CHECK
IN ('worker','time_admin','irrigator','water_admin'),
language_preference, phone, notes, active, last_used_at, …)
- adds brand-scoped RLS, brand index, active index
- nullable field_worker_id on downstream tables, backfilled via
UPDATE … FROM, NULL count verified with RAISE EXCEPTION before
SET NOT NULL
- drops the two old tables and renames FK columns
(irrigator_id → field_worker_id, worker_id → field_worker_id)
- reissues the one-open-clock-in partial unique index against
field_worker_id
Application surface:
- All actions/services now read/write fieldWorkers. Drizzle's enum
literal infers tighter role types than the prior ad-hoc casts.
- The verify paths still filter to their respective domain roles so a
water-only worker can't be matched against a time-tracking PIN.
- API surface preserved: action function names didn't change.
- Seed file updated so QA reseeds work post-deploy.
Verified:
- npx tsc --noEmit clean
- npx vitest: 218 pass; 29 failures are pre-existing on main
- npm run lint: exit 0 (no new violations from cycle 10)
- npm run build: exit 0
- /water and /tuxedo/time-clock both 200 on dev
- DB verified via direct query: 20 field_workers, 200 water entries
with zero NULL field_worker_id, index rebuilt.
PR-reviewer: APPROVED.
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
/**
|
|
* Cycle 10 — Unified field worker table.
|
|
*
|
|
* Replaces `water_irrigators` + `time_tracking_workers` (one row per
|
|
* person with two independent PIN hashes) with one row, one PIN hash,
|
|
* one role vocabulary.
|
|
*
|
|
* Role vocabulary (matches the CHECK constraint in
|
|
* `db/migrations/0097_field_workers.sql`):
|
|
* - `worker` — default; can submit water entries + clock in/out
|
|
* - `time_admin` — can manage time-tracking tasks, settings, and
|
|
* other time workers
|
|
* - `irrigator` — legacy role from `water_irrigators`; same powers
|
|
* as `worker` but kept distinct for UI filtering
|
|
* (the water admin UI shows this label)
|
|
* - `water_admin` — can manage headgates, water workers, and water
|
|
* entries
|
|
*
|
|
* The PIN is a scrypt self-describing hash from `@/lib/water-log-pin`
|
|
* (column name `pin_hash`, NOT `pin` — see cycle 10 migration for the
|
|
* rename).
|
|
*/
|
|
import { boolean, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
import { brands } from "./brands";
|
|
|
|
export const fieldWorkers = pgTable(
|
|
"field_workers",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
brandId: uuid("brand_id")
|
|
.notNull()
|
|
.references(() => brands.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
pinHash: text("pin_hash").notNull(),
|
|
role: text("role", {
|
|
enum: ["worker", "time_admin", "irrigator", "water_admin"],
|
|
})
|
|
.notNull()
|
|
.default("worker"),
|
|
languagePreference: text("language_preference").notNull().default("en"),
|
|
phone: text("phone"),
|
|
notes: text("notes"),
|
|
active: boolean("active").notNull().default(true),
|
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.notNull()
|
|
.defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
.notNull()
|
|
.defaultNow(),
|
|
},
|
|
(t) => ({
|
|
brandIdx: index("field_workers_brand_idx").on(t.brandId),
|
|
}),
|
|
);
|
|
|
|
export type FieldWorker = typeof fieldWorkers.$inferSelect;
|
|
export type NewFieldWorker = typeof fieldWorkers.$inferInsert;
|
|
export type FieldWorkerRole = FieldWorker["role"]; |