mirror of
https://github.com/temetro/temetro.git
synced 2026-08-20 07:02:17 +00:00
backend: add per-clinic staff specialty (Care Team -> patient sheet)
New staff_profile table (org + user, unique) holding a clinician's clinical specialty. GET /api/staff and /api/staff/providers now include specialty; new PATCH /api/staff/:userId (member:update) upserts it. Migration 0023. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,3 +16,4 @@ export * from "./ai-chat.js";
|
||||
export * from "./org-ai-policy.js";
|
||||
export * from "./attachments.js";
|
||||
export * from "./integrations.js";
|
||||
export * from "./staff-profile.js";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
|
||||
import { organization, user } from "./auth.js";
|
||||
|
||||
// Per-member, per-clinic profile extras that Better Auth's member row doesn't
|
||||
// hold. Currently just a doctor's clinical specialty (e.g. "Orthopedist",
|
||||
// "Dentist") which the admin sets in Care Team and surfaces on the patient
|
||||
// sheet for the patient's primary provider. Unique on (org, user) so each
|
||||
// member has at most one profile per clinic.
|
||||
export const staffProfile = pgTable(
|
||||
"staff_profile",
|
||||
{
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
specialty: text("specialty"),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("staff_profile_org_user_idx").on(
|
||||
table.organizationId,
|
||||
table.userId,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
||||
import { auth } from "../auth.js";
|
||||
import { db } from "../db/index.js";
|
||||
import { member, organization, user } from "../db/schema/auth.js";
|
||||
import { staffProfile } from "../db/schema/staff-profile.js";
|
||||
import { HttpError } from "../lib/http-error.js";
|
||||
import { requireAuth, requireOrg, requirePermission } from "../middleware/auth.js";
|
||||
|
||||
@@ -65,9 +66,17 @@ staffRouter.get("/providers", async (req, res, next) => {
|
||||
userId: member.userId,
|
||||
name: user.name,
|
||||
role: member.role,
|
||||
specialty: staffProfile.specialty,
|
||||
})
|
||||
.from(member)
|
||||
.innerJoin(user, eq(user.id, member.userId))
|
||||
.leftJoin(
|
||||
staffProfile,
|
||||
and(
|
||||
eq(staffProfile.userId, member.userId),
|
||||
eq(staffProfile.organizationId, member.organizationId),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(member.organizationId, req.organizationId!),
|
||||
@@ -96,9 +105,17 @@ staffRouter.get(
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
specialty: staffProfile.specialty,
|
||||
})
|
||||
.from(member)
|
||||
.innerJoin(user, eq(user.id, member.userId))
|
||||
.leftJoin(
|
||||
staffProfile,
|
||||
and(
|
||||
eq(staffProfile.userId, member.userId),
|
||||
eq(staffProfile.organizationId, member.organizationId),
|
||||
),
|
||||
)
|
||||
.where(eq(member.organizationId, req.organizationId!))
|
||||
.orderBy(asc(user.name));
|
||||
res.json(rows);
|
||||
@@ -168,3 +185,48 @@ staffRouter.post(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Update a member's clinical specialty. Empty string clears it. Owner/admin
|
||||
// only. Upserts the per-clinic staff_profile row.
|
||||
const specialtyInputSchema = z.object({
|
||||
specialty: z.preprocess(
|
||||
(v) => (typeof v === "string" && v.trim() === "" ? null : v),
|
||||
z.string().trim().max(60).nullable(),
|
||||
),
|
||||
});
|
||||
|
||||
staffRouter.patch(
|
||||
"/:userId",
|
||||
requirePermission({ member: ["update"] }),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const userId = String(req.params.userId ?? "");
|
||||
const { specialty } = specialtyInputSchema.parse(req.body);
|
||||
const organizationId = req.organizationId!;
|
||||
|
||||
// The target must be a member of this clinic.
|
||||
const [target] = await db
|
||||
.select({ id: member.id })
|
||||
.from(member)
|
||||
.where(
|
||||
and(
|
||||
eq(member.organizationId, organizationId),
|
||||
eq(member.userId, userId),
|
||||
),
|
||||
);
|
||||
if (!target) throw new HttpError(404, "Member not found.");
|
||||
|
||||
await db
|
||||
.insert(staffProfile)
|
||||
.values({ organizationId, userId, specialty })
|
||||
.onConflictDoUpdate({
|
||||
target: [staffProfile.organizationId, staffProfile.userId],
|
||||
set: { specialty },
|
||||
});
|
||||
|
||||
res.json({ userId, specialty });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user