diff --git a/backend/drizzle/0003_lame_rawhide_kid.sql b/backend/drizzle/0003_lame_rawhide_kid.sql new file mode 100644 index 0000000..3941d3e --- /dev/null +++ b/backend/drizzle/0003_lame_rawhide_kid.sql @@ -0,0 +1,22 @@ +CREATE TABLE "prescriptions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" text NOT NULL, + "patient_file_number" text DEFAULT '' NOT NULL, + "patient_name" text NOT NULL, + "patient_initials" text NOT NULL, + "medication" text NOT NULL, + "dose" text DEFAULT '' NOT NULL, + "frequency" text NOT NULL, + "prescriber" text NOT NULL, + "prescribed_at" date DEFAULT now() NOT NULL, + "status" text NOT NULL, + "duration" text, + "notes" text, + "created_by" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "prescriptions" ADD CONSTRAINT "prescriptions_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "prescriptions" ADD CONSTRAINT "prescriptions_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "prescriptions_org_idx" ON "prescriptions" USING btree ("organization_id"); \ No newline at end of file diff --git a/backend/drizzle/meta/0003_snapshot.json b/backend/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..0e6319b --- /dev/null +++ b/backend/drizzle/meta/0003_snapshot.json @@ -0,0 +1,1742 @@ +{ + "id": "a0667c3b-380b-46ce-aaaf-eea56a3e5f8c", + "prevId": "6e786663-8ca4-4a3a-bc06-a0e7adbd9408", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit": { + "name": "rate_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rate_limit_key_unique": { + "name": "rate_limit_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patient_allergies": { + "name": "patient_allergies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "patient_id": { + "name": "patient_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "substance": { + "name": "substance", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reaction": { + "name": "reaction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "allergies_patient_idx": { + "name": "allergies_patient_idx", + "columns": [ + { + "expression": "patient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "patient_allergies_patient_id_patients_id_fk": { + "name": "patient_allergies_patient_id_patients_id_fk", + "tableFrom": "patient_allergies", + "tableTo": "patients", + "columnsFrom": [ + "patient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patient_encounters": { + "name": "patient_encounters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "patient_id": { + "name": "patient_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "encounters_patient_idx": { + "name": "encounters_patient_idx", + "columns": [ + { + "expression": "patient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "patient_encounters_patient_id_patients_id_fk": { + "name": "patient_encounters_patient_id_patients_id_fk", + "tableFrom": "patient_encounters", + "tableTo": "patients", + "columnsFrom": [ + "patient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patient_labs": { + "name": "patient_labs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "patient_id": { + "name": "patient_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flag": { + "name": "flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "taken_at": { + "name": "taken_at", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "labs_patient_idx": { + "name": "labs_patient_idx", + "columns": [ + { + "expression": "patient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "patient_labs_patient_id_patients_id_fk": { + "name": "patient_labs_patient_id_patients_id_fk", + "tableFrom": "patient_labs", + "tableTo": "patients", + "columnsFrom": [ + "patient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patient_medications": { + "name": "patient_medications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "patient_id": { + "name": "patient_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dose": { + "name": "dose", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "medications_patient_idx": { + "name": "medications_patient_idx", + "columns": [ + { + "expression": "patient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "patient_medications_patient_id_patients_id_fk": { + "name": "patient_medications_patient_id_patients_id_fk", + "tableFrom": "patient_medications", + "tableTo": "patients", + "columnsFrom": [ + "patient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patients": { + "name": "patients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_number": { + "name": "file_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sex": { + "name": "sex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pcp": { + "name": "pcp", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initials": { + "name": "initials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alerts": { + "name": "alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "vitals_bp": { + "name": "vitals_bp", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vitals_hr": { + "name": "vitals_hr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vitals_temp": { + "name": "vitals_temp", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vitals_spo2": { + "name": "vitals_spo2", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vitals_taken_at": { + "name": "vitals_taken_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vitals_trend": { + "name": "vitals_trend", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "lab_trend": { + "name": "lab_trend", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "patients_org_file_uidx": { + "name": "patients_org_file_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "patients_org_idx": { + "name": "patients_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "patients_organization_id_organization_id_fk": { + "name": "patients_organization_id_organization_id_fk", + "tableFrom": "patients", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "patients_created_by_user_id_fk": { + "name": "patients_created_by_user_id_fk", + "tableFrom": "patients", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patient_problems": { + "name": "patient_problems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "patient_id": { + "name": "patient_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "since": { + "name": "since", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "problems_patient_idx": { + "name": "problems_patient_idx", + "columns": [ + { + "expression": "patient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "patient_problems_patient_id_patients_id_fk": { + "name": "patient_problems_patient_id_patients_id_fk", + "tableFrom": "patient_problems", + "tableTo": "patients", + "columnsFrom": [ + "patient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notes": { + "name": "notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notes_org_author_idx": { + "name": "notes_org_author_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notes_organization_id_organization_id_fk": { + "name": "notes_organization_id_organization_id_fk", + "tableFrom": "notes", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notes_author_id_user_id_fk": { + "name": "notes_author_id_user_id_fk", + "tableFrom": "notes", + "tableTo": "user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appointments": { + "name": "appointments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "patient_file_number": { + "name": "patient_file_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "patient_name": { + "name": "patient_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "patient_initials": { + "name": "patient_initials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "appointments_org_date_idx": { + "name": "appointments_org_date_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "appointments_organization_id_organization_id_fk": { + "name": "appointments_organization_id_organization_id_fk", + "tableFrom": "appointments", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "appointments_created_by_user_id_fk": { + "name": "appointments_created_by_user_id_fk", + "tableFrom": "appointments", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.prescriptions": { + "name": "prescriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "patient_file_number": { + "name": "patient_file_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "patient_name": { + "name": "patient_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "patient_initials": { + "name": "patient_initials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "medication": { + "name": "medication", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dose": { + "name": "dose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "frequency": { + "name": "frequency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prescriber": { + "name": "prescriber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prescribed_at": { + "name": "prescribed_at", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "prescriptions_org_idx": { + "name": "prescriptions_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prescriptions_organization_id_organization_id_fk": { + "name": "prescriptions_organization_id_organization_id_fk", + "tableFrom": "prescriptions", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "prescriptions_created_by_user_id_fk": { + "name": "prescriptions_created_by_user_id_fk", + "tableFrom": "prescriptions", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index ebbbdc9..3d3ecde 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1780849813505, "tag": "0002_fluffy_longshot", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1780850147682, + "tag": "0003_lame_rawhide_kid", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/db/schema/index.ts b/backend/src/db/schema/index.ts index 11bc358..14b126c 100644 --- a/backend/src/db/schema/index.ts +++ b/backend/src/db/schema/index.ts @@ -2,3 +2,4 @@ export * from "./auth.js"; export * from "./patients.js"; export * from "./notes.js"; export * from "./appointments.js"; +export * from "./prescriptions.js"; diff --git a/backend/src/db/schema/prescriptions.ts b/backend/src/db/schema/prescriptions.ts new file mode 100644 index 0000000..1bc9bf1 --- /dev/null +++ b/backend/src/db/schema/prescriptions.ts @@ -0,0 +1,44 @@ +import { + date, + index, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; + +import type { PrescriptionStatus } from "../../types/prescription.js"; +import { organization, user } from "./auth.js"; + +// One row per prescribed medication, scoped to a clinic (organization). Patient +// identity is denormalized (name/initials/file number) so the ledger renders +// without joining. `prescribedAt` defaults to today. +export const prescriptions = pgTable( + "prescriptions", + { + id: uuid("id").primaryKey().defaultRandom(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + patientFileNumber: text("patient_file_number").notNull().default(""), + patientName: text("patient_name").notNull(), + patientInitials: text("patient_initials").notNull(), + medication: text("medication").notNull(), + dose: text("dose").notNull().default(""), + frequency: text("frequency").notNull(), + prescriber: text("prescriber").notNull(), + prescribedAt: date("prescribed_at").defaultNow().notNull(), + status: text("status").$type().notNull(), + duration: text("duration"), + notes: text("notes"), + createdBy: text("created_by").references(() => user.id, { + onDelete: "set null", + }), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => new Date()) + .notNull(), + }, + (t) => [index("prescriptions_org_idx").on(t.organizationId)], +); diff --git a/backend/src/index.ts b/backend/src/index.ts index 7105379..0172519 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -8,6 +8,7 @@ import { errorHandler, notFound } from "./middleware/error.js"; import { appointmentsRouter } from "./routes/appointments.js"; import { notesRouter } from "./routes/notes.js"; import { patientsRouter } from "./routes/patients.js"; +import { prescriptionsRouter } from "./routes/prescriptions.js"; const app = express(); @@ -47,6 +48,7 @@ app.get("/health", (_req, res) => { app.use("/api/patients", patientsRouter); app.use("/api/notes", notesRouter); app.use("/api/appointments", appointmentsRouter); +app.use("/api/prescriptions", prescriptionsRouter); app.use(notFound); app.use(errorHandler); @@ -57,4 +59,5 @@ app.listen(env.PORT, () => { console.log(` • patients: /api/patients`); console.log(` • notes: /api/notes`); console.log(` • appts: /api/appointments`); + console.log(` • rx: /api/prescriptions`); }); diff --git a/backend/src/lib/prescription-validation.ts b/backend/src/lib/prescription-validation.ts new file mode 100644 index 0000000..091e0af --- /dev/null +++ b/backend/src/lib/prescription-validation.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; + +const nonEmpty = z.string().trim().min(1); + +// Payload accepted by POST/PUT /api/prescriptions. The frontend dialog omits +// prescriber / prescribedAt / status on create — the route fills the prescriber +// from the signed-in user, the DB defaults prescribedAt to today, and status +// defaults to "active". +export const prescriptionInputSchema = z.object({ + fileNumber: z.string().trim().default(""), + name: nonEmpty.max(200), + initials: z.string().trim().min(1).max(4), + medication: nonEmpty.max(200), + dose: z.string().trim().max(120).default(""), + frequency: nonEmpty.max(120), + prescriber: z.string().trim().max(200).default(""), + prescribedAt: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD.") + .optional(), + status: z.enum(["active", "completed", "expired"]).default("active"), + duration: z.string().trim().max(120).nullish(), + notes: z.string().max(5000).nullish(), +}); + +export type PrescriptionInput = z.infer; diff --git a/backend/src/routes/prescriptions.ts b/backend/src/routes/prescriptions.ts new file mode 100644 index 0000000..86d3058 --- /dev/null +++ b/backend/src/routes/prescriptions.ts @@ -0,0 +1,83 @@ +import { Router } from "express"; + +import { HttpError } from "../lib/http-error.js"; +import { prescriptionInputSchema } from "../lib/prescription-validation.js"; +import { + requireAuth, + requireOrg, + requirePermission, +} from "../middleware/auth.js"; +import * as service from "../services/prescriptions.js"; + +export const prescriptionsRouter = Router(); + +prescriptionsRouter.use(requireAuth, requireOrg); + +prescriptionsRouter.get( + "/", + requirePermission({ prescription: ["read"] }), + async (req, res, next) => { + try { + res.json(await service.listPrescriptions(req.organizationId!)); + } catch (err) { + next(err); + } + }, +); + +prescriptionsRouter.post( + "/", + requirePermission({ prescription: ["write"] }), + async (req, res, next) => { + try { + const input = prescriptionInputSchema.parse(req.body); + // Default the prescriber to the signed-in clinician when not provided. + input.prescriber = input.prescriber || req.user!.name || "Clinician"; + const created = await service.createPrescription( + req.organizationId!, + req.user!.id, + input, + ); + res.status(201).json(created); + } catch (err) { + next(err); + } + }, +); + +prescriptionsRouter.put( + "/:id", + requirePermission({ prescription: ["write"] }), + async (req, res, next) => { + try { + const input = prescriptionInputSchema.parse(req.body); + input.prescriber = input.prescriber || req.user!.name || "Clinician"; + const updated = await service.updatePrescription( + req.organizationId!, + req.params.id as string, + input, + ); + if (!updated) throw new HttpError(404, "Prescription not found."); + res.json(updated); + } catch (err) { + next(err); + } + }, +); + +prescriptionsRouter.delete( + "/:id", + requirePermission({ prescription: ["delete"] }), + async (req, res, next) => { + try { + const ok = await service.deletePrescription( + req.organizationId!, + req.params.id as string, + ); + if (!ok) throw new HttpError(404, "Prescription not found."); + res.status(204).end(); + } catch (err) { + next(err); + } + }, +); diff --git a/backend/src/services/prescriptions.ts b/backend/src/services/prescriptions.ts new file mode 100644 index 0000000..8eca4a5 --- /dev/null +++ b/backend/src/services/prescriptions.ts @@ -0,0 +1,103 @@ +import { and, desc, eq } from "drizzle-orm"; + +import { db } from "../db/index.js"; +import { prescriptions } from "../db/schema/prescriptions.js"; +import type { PrescriptionInput } from "../lib/prescription-validation.js"; +import type { Prescription } from "../types/prescription.js"; + +type PrescriptionRow = typeof prescriptions.$inferSelect; + +// Postgres throws on a malformed uuid; treat non-uuid ids as "not found". +const UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +function toPrescription(row: PrescriptionRow): Prescription { + return { + id: row.id, + fileNumber: row.patientFileNumber, + name: row.patientName, + initials: row.patientInitials, + medication: row.medication, + dose: row.dose, + frequency: row.frequency, + prescriber: row.prescriber, + prescribedAt: row.prescribedAt, + status: row.status, + duration: row.duration, + notes: row.notes, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }; +} + +function columns(orgId: string, input: PrescriptionInput, createdBy?: string) { + return { + organizationId: orgId, + patientFileNumber: input.fileNumber, + patientName: input.name, + patientInitials: input.initials, + medication: input.medication, + dose: input.dose, + frequency: input.frequency, + prescriber: input.prescriber, + status: input.status, + duration: input.duration ?? null, + notes: input.notes ?? null, + // Only set prescribedAt when supplied; otherwise the column default (today). + ...(input.prescribedAt ? { prescribedAt: input.prescribedAt } : {}), + ...(createdBy ? { createdBy } : {}), + }; +} + +export async function listPrescriptions( + orgId: string, +): Promise { + const rows = await db + .select() + .from(prescriptions) + .where(eq(prescriptions.organizationId, orgId)) + .orderBy(desc(prescriptions.prescribedAt), desc(prescriptions.createdAt)); + return rows.map(toPrescription); +} + +export async function createPrescription( + orgId: string, + userId: string, + input: PrescriptionInput, +): Promise { + const [row] = await db + .insert(prescriptions) + .values(columns(orgId, input, userId)) + .returning(); + return toPrescription(row!); +} + +export async function updatePrescription( + orgId: string, + id: string, + input: PrescriptionInput, +): Promise { + if (!UUID_RE.test(id)) return null; + const [row] = await db + .update(prescriptions) + .set(columns(orgId, input)) + .where( + and(eq(prescriptions.id, id), eq(prescriptions.organizationId, orgId)), + ) + .returning(); + return row ? toPrescription(row) : null; +} + +export async function deletePrescription( + orgId: string, + id: string, +): Promise { + if (!UUID_RE.test(id)) return false; + const deleted = await db + .delete(prescriptions) + .where( + and(eq(prescriptions.id, id), eq(prescriptions.organizationId, orgId)), + ) + .returning({ id: prescriptions.id }); + return deleted.length > 0; +} diff --git a/backend/src/types/prescription.ts b/backend/src/types/prescription.ts new file mode 100644 index 0000000..adfe6d7 --- /dev/null +++ b/backend/src/types/prescription.ts @@ -0,0 +1,21 @@ +// The canonical Prescription shape returned by the API. Mirrors the frontend +// `lib/prescriptions.ts` Prescription type. Scoped to the active clinic; patient +// fields are denormalized for display and `fileNumber` links to a patient. +export type PrescriptionStatus = "active" | "completed" | "expired"; + +export type Prescription = { + id: string; + fileNumber: string; + name: string; + initials: string; + medication: string; + dose: string; + frequency: string; + prescriber: string; + prescribedAt: string; // YYYY-MM-DD + status: PrescriptionStatus; + duration: string | null; + notes: string | null; + createdAt: string; + updatedAt: string; +}; diff --git a/frontend/components/prescriptions/add-prescription-dialog.tsx b/frontend/components/prescriptions/add-prescription-dialog.tsx index f7f6e0a..2dc6511 100644 --- a/frontend/components/prescriptions/add-prescription-dialog.tsx +++ b/frontend/components/prescriptions/add-prescription-dialog.tsx @@ -111,8 +111,8 @@ function Field({ label, children }: { label: string; children: ReactNode }) { // Compact "New prescription" dialog. The patient is chosen via a quick search by // name or file number (same pattern as the appointment dialog); the rest is the -// medication detail. Prescriptions are mock-only, so the new entry is handed back -// to the page via onAdd. +// medication detail. The new entry is handed back to the page via onAdd, which +// persists it through the prescriptions API. export function AddPrescriptionDialog({ open, onOpenChange, diff --git a/frontend/components/prescriptions/prescription-detail-sheet.tsx b/frontend/components/prescriptions/prescription-detail-sheet.tsx index b4809e2..1759de5 100644 --- a/frontend/components/prescriptions/prescription-detail-sheet.tsx +++ b/frontend/components/prescriptions/prescription-detail-sheet.tsx @@ -10,6 +10,7 @@ import { SheetTitle, } from "@/components/ui/sheet"; import type { Prescription, RxStatus } from "@/components/prescriptions/prescriptions-view"; +import { formatPrescribedAt } from "@/lib/prescriptions"; const statusVariant: Record< RxStatus, @@ -28,7 +29,7 @@ const statusLabel: Record = { // Right-side Sheet showing one prescription's full detail, opened by clicking a // row in the Prescriptions list (mirrors the Patients table → side Sheet -// pattern). Prescriptions live in local state, so the record is passed in. +// pattern). The selected record is passed in from the page. export function PrescriptionDetailSheet({ rx, open, @@ -82,7 +83,9 @@ export function PrescriptionDetailSheet({
Prescriber
{rx.prescriber}
Date
-
{rx.date}
+
+ {formatPrescribedAt(rx.prescribedAt)} +
Status
{statusLabel[rx.status]}
diff --git a/frontend/components/prescriptions/prescriptions-view.tsx b/frontend/components/prescriptions/prescriptions-view.tsx index 216fa67..88a9819 100644 --- a/frontend/components/prescriptions/prescriptions-view.tsx +++ b/frontend/components/prescriptions/prescriptions-view.tsx @@ -1,7 +1,7 @@ "use client"; import { CircleCheck, Clock, Pill, Plus } from "lucide-react"; -import { type ReactNode, useState } from "react"; +import { type ReactNode, useEffect, useMemo, useState } from "react"; import { AddPrescriptionDialog, @@ -12,25 +12,16 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; +import { + type Prescription, + type RxStatus, + createPrescription, + formatPrescribedAt, + listPrescriptions, +} from "@/lib/prescriptions"; +import { notify } from "@/lib/toast"; -// All figures here are mock/placeholder data — there is no prescriptions backend. -// They illustrate the Prescriptions layout. - -export type RxStatus = "active" | "completed" | "expired"; - -export type Prescription = { - fileNumber: string; - name: string; - initials: string; - medication: string; - dose: string; - frequency: string; - prescriber: string; - date: string; - status: RxStatus; - duration?: string; - notes?: string; -}; +export type { Prescription, RxStatus } from "@/lib/prescriptions"; const statusVariant: Record< RxStatus, @@ -47,70 +38,6 @@ const statusLabel: Record = { expired: "Expired", }; -const kpis = [ - { label: "Active", value: "8", icon: Pill }, - { label: "Due refill", value: "3", icon: Clock }, - { label: "Completed", value: "27", icon: CircleCheck }, -]; - -const initial: Prescription[] = [ - { - fileNumber: "10293", - name: "Amina Yusuf", - initials: "AY", - medication: "Lisinopril", - dose: "10 mg", - frequency: "Once daily", - prescriber: "Dr. Okafor", - date: "Jun 5, 2026", - status: "active", - }, - { - fileNumber: "10311", - name: "Daniel Mensah", - initials: "DM", - medication: "Metformin", - dose: "500 mg", - frequency: "Twice daily", - prescriber: "Dr. Okafor", - date: "Jun 4, 2026", - status: "active", - }, - { - fileNumber: "10342", - name: "Leila Haddad", - initials: "LH", - medication: "Amoxicillin", - dose: "500 mg", - frequency: "Three times daily", - prescriber: "Dr. Stein", - date: "May 28, 2026", - status: "completed", - }, - { - fileNumber: "10358", - name: "Carlos Rivera", - initials: "CR", - medication: "Atorvastatin", - dose: "20 mg", - frequency: "Once daily", - prescriber: "Dr. Okafor", - date: "May 12, 2026", - status: "expired", - }, - { - fileNumber: "10377", - name: "Priya Nair", - initials: "PN", - medication: "Salbutamol inhaler", - dose: "100 mcg", - frequency: "As needed", - prescriber: "Dr. Stein", - date: "Jun 1, 2026", - status: "active", - }, -]; - function Kpi({ label, value, @@ -165,7 +92,9 @@ function RxRow({ rx, onOpen }: { rx: Prescription; onOpen: () => void }) {
{rx.prescriber} - {rx.date} + + {formatPrescribedAt(rx.prescribedAt)} +
{statusLabel[rx.status]} @@ -196,46 +125,76 @@ function Section({ export function PrescriptionsView() { const [addOpen, setAddOpen] = useState(false); - const [list, setList] = useState(initial); + const [list, setList] = useState([]); const [selected, setSelected] = useState(null); const [sheetOpen, setSheetOpen] = useState(false); + useEffect(() => { + let active = true; + listPrescriptions() + .then((data) => { + if (active) setList(data); + }) + .catch(() => { + /* api-client redirects on 401; otherwise leave the list empty */ + }); + return () => { + active = false; + }; + }, []); + const openRx = (rx: Prescription) => { setSelected(rx); setSheetOpen(true); }; - // Insert a new (mock) prescription at the top of the list, marked active. - const addPrescription = (rx: NewPrescription) => { - setList((prev) => [ - { + // Persist a new prescription, then add the saved record to the top of the list. + const addPrescription = async (rx: NewPrescription) => { + try { + const created = await createPrescription({ fileNumber: rx.fileNumber, name: rx.name, initials: rx.initials, medication: rx.medication, dose: rx.dose, frequency: rx.frequency, - duration: rx.duration || undefined, - notes: rx.notes || undefined, - prescriber: "Dr. Okafor", - date: new Date().toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }), - status: "active" as const, - }, - ...prev, - ]); + duration: rx.duration || null, + notes: rx.notes || null, + }); + setList((prev) => [created, ...prev]); + } catch { + notify.error("Couldn't add prescription", "Please try again."); + } }; + const kpis = useMemo( + () => [ + { + label: "Active", + value: String(list.filter((r) => r.status === "active").length), + icon: Pill, + }, + { + label: "Completed", + value: String(list.filter((r) => r.status === "completed").length), + icon: CircleCheck, + }, + { + label: "Expired", + value: String(list.filter((r) => r.status === "expired").length), + icon: Clock, + }, + ], + [list], + ); + return (

Prescriptions

- Medications prescribed across the clinic. Sample data. + Medications prescribed across the clinic.