feat: patient blood type & phone + clinic location setting (v0.9.0)

Patient record:
- Add `bloodType` and `phone` to the patient model (schema, canonical types on
  both backend + frontend, zod validation). `phone` is a demographic field
  (reception can read/write); `bloodType` is clinical PHI, redacted for the
  reception role. Surface both in the record sheet, chat summary card, and the
  add/edit patient form. Migration 0033.

Clinic location:
- New org-scoped `clinic_settings` table (address/city/country + optional
  lat/long), service, and routes: GET /api/clinic/settings (any clinician) and
  PUT /api/clinic/location (owner/admin). Edited in Settings → Signing → Clinic
  location. Consumed later by the wallet app. Migration 0034.

i18n:
- Translate all new keys into every shipped locale (en/de/fr/ar/so) and document
  the "translate into every locale" rule in frontend/CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-06 22:07:27 +03:00
parent 01dbc07e92
commit 36461a5498
31 changed files with 9885 additions and 5 deletions
+82
View File
@@ -0,0 +1,82 @@
import { eq } from "drizzle-orm";
import { db } from "../db/index.js";
import { clinicSettings } from "../db/schema/clinic-settings.js";
export type ClinicLocation = {
address: string;
city: string;
country: string;
latitude: number | null;
longitude: number | null;
};
export type ClinicSettingsView = {
location: ClinicLocation;
};
const EMPTY_LOCATION: ClinicLocation = {
address: "",
city: "",
country: "",
latitude: null,
longitude: null,
};
type ClinicSettingsRow = typeof clinicSettings.$inferSelect;
function toView(row: ClinicSettingsRow | undefined): ClinicSettingsView {
if (!row) return { location: { ...EMPTY_LOCATION } };
return {
location: {
address: row.address,
city: row.city,
country: row.country,
latitude: row.latitude,
longitude: row.longitude,
},
};
}
// Read a clinic's settings. Returns empty defaults when no row exists yet, so
// the panel always renders.
export async function getClinicSettings(
orgId: string,
): Promise<ClinicSettingsView> {
const [row] = await db
.select()
.from(clinicSettings)
.where(eq(clinicSettings.organizationId, orgId))
.limit(1);
return toView(row);
}
// Upsert the clinic's location (address + optional coordinates).
export async function setClinicLocation(
orgId: string,
location: ClinicLocation,
): Promise<ClinicSettingsView> {
const values = {
organizationId: orgId,
address: location.address,
city: location.city,
country: location.country,
latitude: location.latitude,
longitude: location.longitude,
};
const [row] = await db
.insert(clinicSettings)
.values(values)
.onConflictDoUpdate({
target: clinicSettings.organizationId,
set: {
address: values.address,
city: values.city,
country: values.country,
latitude: values.latitude,
longitude: values.longitude,
},
})
.returning();
return toView(row);
}
+9
View File
@@ -53,6 +53,8 @@ function toPatient(row: PatientRow, children: Children): Patient {
primaryProviderId: row.primaryProviderId,
status: row.status,
initials: row.initials,
phone: row.phone,
bloodType: row.bloodType,
allergies: children.allergies,
alerts: row.alerts,
medications: children.medications,
@@ -82,6 +84,8 @@ const EMPTY_TREND: Trend = { label: "", unit: "", points: [] };
function redactClinical(patient: Patient): Patient {
return {
...patient,
// bloodType is clinical PHI; phone is a demographic/contact field and stays.
bloodType: "",
allergies: [],
alerts: [],
medications: [],
@@ -116,6 +120,8 @@ function patientColumns(orgId: string, input: PatientInput, createdBy?: string)
primaryProviderId: input.primaryProviderId ?? null,
status: input.status,
initials: input.initials,
phone: input.phone,
bloodType: input.bloodType,
alerts: input.alerts,
vitalsBp: input.vitals.bp,
vitalsHr: input.vitals.hr,
@@ -147,6 +153,8 @@ function demographicColumns(
primaryProviderId: input.primaryProviderId ?? null,
status: input.status,
initials: input.initials,
phone: input.phone,
bloodType: "",
source: input.source,
alerts: [] as string[],
vitalsBp: "",
@@ -172,6 +180,7 @@ function demographicUpdateColumns(input: PatientInput) {
primaryProviderId: input.primaryProviderId ?? null,
status: input.status,
initials: input.initials,
phone: input.phone,
};
}