@omniradiology/omnirad 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +438 -0
  2. package/app/api/ai-config/route.ts +131 -0
  3. package/app/api/ai-config/test/route.ts +49 -0
  4. package/app/api/auth/auto-login/route.ts +66 -0
  5. package/app/api/auth/check/route.ts +17 -0
  6. package/app/api/auth/login/route.ts +72 -0
  7. package/app/api/auth/logout/route.ts +25 -0
  8. package/app/api/auth/me/route.ts +75 -0
  9. package/app/api/auth/password/route.ts +49 -0
  10. package/app/api/auth/setup/route.ts +63 -0
  11. package/app/api/auth/users/route.ts +100 -0
  12. package/app/api/auth/wipe/route.ts +27 -0
  13. package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
  14. package/app/api/compliance/audit/route.ts +110 -0
  15. package/app/api/compliance/export/patient/[id]/route.ts +108 -0
  16. package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
  17. package/app/api/compliance/settings/route.ts +93 -0
  18. package/app/api/copilot/annotate/route.ts +94 -0
  19. package/app/api/copilot/chat/route.ts +238 -0
  20. package/app/api/copilot/history/route.ts +95 -0
  21. package/app/api/copilot/reports/route.ts +81 -0
  22. package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
  23. package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
  24. package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
  25. package/app/api/fhir/Patient/[id]/route.ts +26 -0
  26. package/app/api/fhir/ServiceRequest/route.ts +85 -0
  27. package/app/api/fhir/config/route.ts +102 -0
  28. package/app/api/fhir/config/test-connection/route.ts +49 -0
  29. package/app/api/fhir/metadata/route.ts +51 -0
  30. package/app/api/pacs/metadata/route.ts +32 -0
  31. package/app/api/pacs/qido/instances/route.ts +39 -0
  32. package/app/api/pacs/qido/series/route.ts +38 -0
  33. package/app/api/pacs/qido/studies/route.ts +37 -0
  34. package/app/api/pacs/test/route.ts +30 -0
  35. package/app/api/pacs/wado/render/route.ts +51 -0
  36. package/app/api/patients/[id]/reports/route.ts +18 -0
  37. package/app/api/patients/[id]/route.ts +43 -0
  38. package/app/api/patients/merge/route.ts +57 -0
  39. package/app/api/patients/route.ts +67 -0
  40. package/app/api/patients/search/route.ts +25 -0
  41. package/app/api/reports/[id]/route.ts +84 -0
  42. package/app/api/reports/[id]/status/route.ts +87 -0
  43. package/app/api/reports/clear/route.ts +16 -0
  44. package/app/api/reports/route.ts +112 -0
  45. package/app/api/segmentation-config/route.ts +238 -0
  46. package/app/api/settings/route.ts +245 -0
  47. package/app/api/settings/test-supabase/route.ts +103 -0
  48. package/app/api/upload/route.ts +48 -0
  49. package/app/copilot/page.tsx +30 -0
  50. package/app/globals.css +141 -0
  51. package/app/history/page.tsx +242 -0
  52. package/app/icon.svg +3 -0
  53. package/app/layout.tsx +47 -0
  54. package/app/login/page.tsx +175 -0
  55. package/app/pacs/page.tsx +78 -0
  56. package/app/page.tsx +125 -0
  57. package/app/patients/[id]/page.tsx +315 -0
  58. package/app/patients/page.tsx +110 -0
  59. package/app/profile/page.tsx +208 -0
  60. package/app/reports/page.tsx +432 -0
  61. package/app/settings/page.tsx +454 -0
  62. package/app/setup/page.tsx +199 -0
  63. package/components/admin/AuditLogTable.tsx +293 -0
  64. package/components/copilot/ActivityIndicator.tsx +215 -0
  65. package/components/copilot/ChatHistoryPanel.tsx +140 -0
  66. package/components/copilot/ChatMessage.tsx +251 -0
  67. package/components/copilot/ClickableReference.tsx +40 -0
  68. package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
  69. package/components/copilot/CopilotPanel.tsx +311 -0
  70. package/components/copilot/FindingsList.tsx +75 -0
  71. package/components/copilot/ViewerPanel.tsx +460 -0
  72. package/components/copilot/WorkspaceLayout.tsx +398 -0
  73. package/components/dashboard/AIConfigPanel.tsx +339 -0
  74. package/components/dashboard/AppearancePanel.tsx +491 -0
  75. package/components/dashboard/ApprovalModal.tsx +163 -0
  76. package/components/dashboard/CollaborationPanel.tsx +134 -0
  77. package/components/dashboard/CopilotConfigPanel.tsx +337 -0
  78. package/components/dashboard/DicomViewer.tsx +645 -0
  79. package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
  80. package/components/dashboard/FullReportOverlay.tsx +269 -0
  81. package/components/dashboard/ImageViewer.tsx +541 -0
  82. package/components/dashboard/PatientForm.tsx +597 -0
  83. package/components/dashboard/RejectionModal.tsx +74 -0
  84. package/components/dashboard/ReportEditor.tsx +160 -0
  85. package/components/dashboard/ReportTemplates.tsx +729 -0
  86. package/components/dashboard/ReportView.tsx +539 -0
  87. package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
  88. package/components/dashboard/StudyPlaceholder.tsx +17 -0
  89. package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
  90. package/components/dashboard/UserManagementPanel.tsx +272 -0
  91. package/components/layout/ClientLayout.tsx +39 -0
  92. package/components/layout/Header.tsx +20 -0
  93. package/components/layout/Sidebar.tsx +119 -0
  94. package/components/pacs/PacsImageViewerModal.tsx +121 -0
  95. package/components/pacs/PacsSearchFilters.tsx +117 -0
  96. package/components/pacs/PacsSeriesViewer.tsx +190 -0
  97. package/components/pacs/PacsStudyTable.tsx +113 -0
  98. package/components/patients/patient-card.tsx +117 -0
  99. package/components/patients/patient-header.tsx +122 -0
  100. package/components/patients/patient-search.tsx +137 -0
  101. package/components/patients/patient-timeline.tsx +153 -0
  102. package/components/settings/ComplianceSettingsPanel.tsx +278 -0
  103. package/components/settings/SecurityPanel.tsx +418 -0
  104. package/components/ui/badge.tsx +19 -0
  105. package/components/ui/basic.tsx +156 -0
  106. package/db/index.ts +350 -0
  107. package/db/migrations/0000_odd_quasimodo.sql +117 -0
  108. package/db/migrations/meta/0000_snapshot.json +778 -0
  109. package/db/migrations/meta/_journal.json +13 -0
  110. package/db/schema.ts +239 -0
  111. package/drizzle.config.ts +10 -0
  112. package/lib/api.ts +689 -0
  113. package/lib/auth.ts +22 -0
  114. package/lib/copilot/action-executor.ts +94 -0
  115. package/lib/copilot/action-types.ts +72 -0
  116. package/lib/copilot/coordinate-mapper.ts +84 -0
  117. package/lib/dicomImageExtractor.ts +103 -0
  118. package/lib/dicomMetadataParser.ts +111 -0
  119. package/lib/fhir/client.ts +25 -0
  120. package/lib/fhir/constants.ts +21 -0
  121. package/lib/fhir/diagnostic-report.ts +88 -0
  122. package/lib/fhir/helpers.ts +73 -0
  123. package/lib/fhir/imaging-study.ts +49 -0
  124. package/lib/fhir/patient.ts +55 -0
  125. package/lib/fhir/service-request.ts +85 -0
  126. package/lib/fhir.ts +6 -0
  127. package/lib/pacs/dicom-utils.ts +72 -0
  128. package/lib/pacs/dicomweb.ts +72 -0
  129. package/lib/pacs/server-utils.ts +37 -0
  130. package/lib/patients.ts +25 -0
  131. package/lib/pdfHelper.ts +119 -0
  132. package/lib/reportHtmlGenerator.ts +581 -0
  133. package/lib/security/audit.ts +180 -0
  134. package/lib/security/authz.ts +246 -0
  135. package/lib/security/phi-redaction.ts +156 -0
  136. package/lib/security/rate-limit.ts +106 -0
  137. package/lib/security/secrets.ts +179 -0
  138. package/lib/supabase.ts +72 -0
  139. package/lib/utils.ts +6 -0
  140. package/next.config.ts +35 -0
  141. package/package.json +76 -0
  142. package/public/file.svg +1 -0
  143. package/public/globe.svg +1 -0
  144. package/public/logo.svg +8 -0
  145. package/public/next.svg +1 -0
  146. package/public/omnirad-favicon.svg +8 -0
  147. package/public/vercel.svg +1 -0
  148. package/public/window.svg +1 -0
  149. package/tsconfig.json +34 -0
  150. package/types/copilot-viewer.ts +155 -0
  151. package/types/copilot.ts +105 -0
  152. package/types/fhir.ts +21 -0
  153. package/types/html2pdf.d.ts +20 -0
  154. package/types/index.ts +139 -0
  155. package/types/pacs.ts +41 -0
@@ -0,0 +1,32 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getOrthancHeaders } from "@/lib/pacs/server-utils";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { headers, baseUrl } = await getOrthancHeaders();
7
+ const searchParams = request.nextUrl.searchParams;
8
+ const studyUid = searchParams.get('studyUid');
9
+
10
+ if (!studyUid) {
11
+ return NextResponse.json({ error: "Missing studyUid parameter" }, { status: 400 });
12
+ }
13
+
14
+ const orthancUrl = new URL(`${baseUrl}/studies/${studyUid}/metadata`);
15
+
16
+ const res = await fetch(orthancUrl.toString(), {
17
+ method: "GET",
18
+ headers,
19
+ });
20
+
21
+ if (!res.ok) {
22
+ const errText = await res.text();
23
+ throw new Error(`Orthanc returned ${res.status}: ${errText}`);
24
+ }
25
+
26
+ const data = await res.json();
27
+ return NextResponse.json(data);
28
+ } catch (e: any) {
29
+ console.error("PACS Metadata Proxy Error:", e);
30
+ return NextResponse.json({ error: e.message }, { status: 500 });
31
+ }
32
+ }
@@ -0,0 +1,39 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getOrthancHeaders } from "@/lib/pacs/server-utils";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { headers, baseUrl } = await getOrthancHeaders();
7
+ const searchParams = request.nextUrl.searchParams;
8
+ const studyUid = searchParams.get('studyUid');
9
+ const seriesUid = searchParams.get('seriesUid');
10
+
11
+ if (!studyUid || !seriesUid) {
12
+ return NextResponse.json({ error: "Missing studyUid or seriesUid parameter" }, { status: 400 });
13
+ }
14
+
15
+ const orthancUrl = new URL(`${baseUrl}/studies/${studyUid}/series/${seriesUid}/instances`);
16
+
17
+ searchParams.forEach((value, key) => {
18
+ if (key !== 'studyUid' && key !== 'seriesUid') {
19
+ orthancUrl.searchParams.append(key, value);
20
+ }
21
+ });
22
+
23
+ const res = await fetch(orthancUrl.toString(), {
24
+ method: "GET",
25
+ headers,
26
+ });
27
+
28
+ if (!res.ok) {
29
+ const errText = await res.text();
30
+ throw new Error(`Orthanc returned ${res.status}: ${errText}`);
31
+ }
32
+
33
+ const data = await res.json();
34
+ return NextResponse.json(data);
35
+ } catch (e: any) {
36
+ console.error("PACS Instances Proxy Error:", e);
37
+ return NextResponse.json({ error: e.message }, { status: 500 });
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getOrthancHeaders } from "@/lib/pacs/server-utils";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { headers, baseUrl } = await getOrthancHeaders();
7
+ const searchParams = request.nextUrl.searchParams;
8
+ const studyUid = searchParams.get('studyUid');
9
+
10
+ if (!studyUid) {
11
+ return NextResponse.json({ error: "Missing studyUid parameter" }, { status: 400 });
12
+ }
13
+
14
+ const orthancUrl = new URL(`${baseUrl}/studies/${studyUid}/series`);
15
+
16
+ searchParams.forEach((value, key) => {
17
+ if (key !== 'studyUid') {
18
+ orthancUrl.searchParams.append(key, value);
19
+ }
20
+ });
21
+
22
+ const res = await fetch(orthancUrl.toString(), {
23
+ method: "GET",
24
+ headers,
25
+ });
26
+
27
+ if (!res.ok) {
28
+ const errText = await res.text();
29
+ throw new Error(`Orthanc returned ${res.status}: ${errText}`);
30
+ }
31
+
32
+ const data = await res.json();
33
+ return NextResponse.json(data);
34
+ } catch (e: any) {
35
+ console.error("PACS Series Proxy Error:", e);
36
+ return NextResponse.json({ error: e.message }, { status: 500 });
37
+ }
38
+ }
@@ -0,0 +1,37 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getOrthancHeaders } from "@/lib/pacs/server-utils";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { headers, baseUrl } = await getOrthancHeaders();
7
+ const searchParams = request.nextUrl.searchParams;
8
+
9
+ // E.g. /studies?PatientName=*John*&PatientID=123
10
+ const orthancUrl = new URL(baseUrl + "/studies");
11
+
12
+ searchParams.forEach((value, key) => {
13
+ orthancUrl.searchParams.append(key, value);
14
+ });
15
+
16
+ // Add limit by default if not specified
17
+ if (!orthancUrl.searchParams.has('limit')) {
18
+ orthancUrl.searchParams.append('limit', '50');
19
+ }
20
+
21
+ const res = await fetch(orthancUrl.toString(), {
22
+ method: "GET",
23
+ headers,
24
+ });
25
+
26
+ if (!res.ok) {
27
+ const errText = await res.text();
28
+ throw new Error(`Orthanc returned ${res.status}: ${errText}`);
29
+ }
30
+
31
+ const data = await res.json();
32
+ return NextResponse.json(data);
33
+ } catch (e: any) {
34
+ console.error("PACS Studies Proxy Error:", e);
35
+ return NextResponse.json({ error: e.message }, { status: 500 });
36
+ }
37
+ }
@@ -0,0 +1,30 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getOrthancHeaders } from "@/lib/pacs/server-utils";
3
+
4
+ export async function GET() {
5
+ try {
6
+ const { headers, baseUrl } = await getOrthancHeaders();
7
+ // The most basic connectivity test is fetching the root system info or studies with limit 1
8
+ const rootUrl = new URL(baseUrl);
9
+ const testUrl = new URL("/studies?limit=1", baseUrl);
10
+
11
+ console.log("Testing connection to:", testUrl.toString());
12
+
13
+ const res = await fetch(testUrl.toString(), {
14
+ method: "GET",
15
+ headers,
16
+ });
17
+
18
+ if (!res.ok) {
19
+ const errText = await res.text();
20
+ throw new Error(`Orthanc returned ${res.status}: ${errText}`);
21
+ }
22
+
23
+ const data = await res.json();
24
+
25
+ return NextResponse.json({ success: true, count: data?.length || 0 });
26
+ } catch (e: any) {
27
+ console.error("PACS Test Route Error:", e);
28
+ return NextResponse.json({ success: false, error: e.message }, { status: 500 });
29
+ }
30
+ }
@@ -0,0 +1,51 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getOrthancHeaders } from "@/lib/pacs/server-utils";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { headers, baseUrl } = await getOrthancHeaders();
7
+ const searchParams = request.nextUrl.searchParams;
8
+ const studyUid = searchParams.get('studyUid');
9
+ const seriesUid = searchParams.get('seriesUid');
10
+ const instanceUid = searchParams.get('instanceUid');
11
+ const frame = searchParams.get('frame');
12
+
13
+ if (!studyUid || !seriesUid || !instanceUid) {
14
+ return new NextResponse("Missing studyUid, seriesUid, or instanceUid parameter", { status: 400 });
15
+ }
16
+
17
+ // If a frame number is specified, fetch that specific frame from a multi-frame instance
18
+ let renderPath = `${baseUrl}/studies/${studyUid}/series/${seriesUid}/instances/${instanceUid}`;
19
+ if (frame) {
20
+ renderPath += `/frames/${frame}/rendered`;
21
+ } else {
22
+ renderPath += `/rendered`;
23
+ }
24
+ const orthancUrl = new URL(renderPath);
25
+
26
+ // Ask for jpeg explicitly if Orthanc supports it via accept headers
27
+ const fetchHeaders = { ...headers, "Accept": "image/jpeg" };
28
+
29
+ const res = await fetch(orthancUrl.toString(), {
30
+ method: "GET",
31
+ headers: fetchHeaders,
32
+ });
33
+
34
+ if (!res.ok) {
35
+ const errText = await res.text();
36
+ throw new Error(`Orthanc returned ${res.status}: ${errText}`);
37
+ }
38
+
39
+ // Return the binary body directly
40
+ const contentType = res.headers.get("content-type") || "image/jpeg";
41
+ return new NextResponse(res.body, {
42
+ headers: {
43
+ "Content-Type": contentType,
44
+ "Cache-Control": "public, max-age=86400",
45
+ },
46
+ });
47
+ } catch (e: any) {
48
+ console.error("PACS Render Proxy Error:", e);
49
+ return new NextResponse(e.message, { status: 500 });
50
+ }
51
+ }
@@ -0,0 +1,18 @@
1
+ import { db } from "@/db";
2
+ import { reports } from "@/db/schema";
3
+ import { eq, desc } from "drizzle-orm";
4
+ import { NextResponse } from "next/server";
5
+
6
+ export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ try {
8
+ const { id } = await params;
9
+ const data = await db.select()
10
+ .from(reports)
11
+ .where(eq(reports.patientId, id))
12
+ .orderBy(desc(reports.createdAt));
13
+
14
+ return NextResponse.json(data);
15
+ } catch (e: any) {
16
+ return NextResponse.json({ error: e.message }, { status: 500 });
17
+ }
18
+ }
@@ -0,0 +1,43 @@
1
+ import { db } from "@/db";
2
+ import { patients, reports } from "@/db/schema";
3
+ import { eq, desc } from "drizzle-orm";
4
+ import { NextResponse } from "next/server";
5
+
6
+ export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ try {
8
+ const { id } = await params;
9
+ const result = await db.select().from(patients).where(eq(patients.id, id)).limit(1);
10
+ if (result.length === 0) {
11
+ return NextResponse.json({ error: "Patient not found" }, { status: 404 });
12
+ }
13
+ return NextResponse.json(result[0]);
14
+ } catch (e: any) {
15
+ return NextResponse.json({ error: e.message }, { status: 500 });
16
+ }
17
+ }
18
+
19
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
20
+ try {
21
+ const { id } = await params;
22
+ const body = await req.json();
23
+ const updates = {
24
+ ...body,
25
+ updatedAt: new Date().toISOString()
26
+ };
27
+ await db.update(patients).set(updates).where(eq(patients.id, id));
28
+ return NextResponse.json({ success: true });
29
+ } catch (e: any) {
30
+ return NextResponse.json({ error: e.message }, { status: 500 });
31
+ }
32
+ }
33
+
34
+ export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
35
+ try {
36
+ const { id } = await params;
37
+ // The cascading delete is on the DB schema, so deleting the patient should delete reports.
38
+ await db.delete(patients).where(eq(patients.id, id));
39
+ return NextResponse.json({ success: true });
40
+ } catch (e: any) {
41
+ return NextResponse.json({ error: e.message }, { status: 500 });
42
+ }
43
+ }
@@ -0,0 +1,57 @@
1
+ import { db } from "@/db";
2
+ import { patients, reports } from "@/db/schema";
3
+ import { isNull, eq } from "drizzle-orm";
4
+ import { NextResponse } from "next/server";
5
+ import { randomUUID } from "crypto";
6
+
7
+ export async function POST(req: Request) {
8
+ try {
9
+ const body = await req.json();
10
+
11
+ if (body.action === "auto-migrate") {
12
+ const nullReports = await db.select().from(reports).where(isNull(reports.patientId));
13
+ let migratedCount = 0;
14
+
15
+ for (const r of nullReports) {
16
+ if (!r.reportData) continue;
17
+ try {
18
+ const parsed = JSON.parse(r.reportData);
19
+ const pName = r.patientName || parsed.patient?.name;
20
+ if (!pName) continue;
21
+
22
+ // Does this patient exist?
23
+ const existing = await db.select().from(patients).where(eq(patients.patientName, pName)).limit(1);
24
+
25
+ let pId = "";
26
+ if (existing.length > 0) {
27
+ pId = existing[0].id;
28
+ } else {
29
+ pId = randomUUID();
30
+ await db.insert(patients).values({
31
+ id: pId,
32
+ patientName: pName,
33
+ patientIdNumber: parsed.patient?.patient_id || null,
34
+ gender: parsed.patient?.gender || null,
35
+ dob: parsed.patient?.age ? null : null, // Not inferring DOB from age directly
36
+ createdAt: new Date().toISOString(),
37
+ updatedAt: new Date().toISOString()
38
+ });
39
+ }
40
+
41
+ // Link report
42
+ await db.update(reports).set({ patientId: pId }).where(eq(reports.id, r.id));
43
+ migratedCount++;
44
+ } catch(err) {
45
+ console.error("[OmniRad] Migration error on report", r.id, err);
46
+ }
47
+ }
48
+
49
+ return NextResponse.json({ success: true, migrated: migratedCount });
50
+ }
51
+
52
+ return NextResponse.json({ error: "Invalid action" }, { status: 400 });
53
+ } catch (e: any) {
54
+ console.error(e);
55
+ return NextResponse.json({ error: e.message }, { status: 500 });
56
+ }
57
+ }
@@ -0,0 +1,67 @@
1
+ import { db } from "@/db";
2
+ import { patients, reports } from "@/db/schema";
3
+ import { NextResponse } from "next/server";
4
+ import { randomUUID } from "crypto";
5
+ import { desc, like, or } from "drizzle-orm";
6
+
7
+ export async function GET(req: Request) {
8
+ try {
9
+ const url = new URL(req.url);
10
+ const search = url.searchParams.get("search");
11
+
12
+ let query = db.select().from(patients);
13
+
14
+ if (search) {
15
+ query = query.where(
16
+ or(
17
+ like(patients.patientName, `%${search}%`),
18
+ like(patients.patientIdNumber, `%${search}%`)
19
+ )
20
+ ) as any;
21
+ }
22
+
23
+ const data = await query.orderBy(desc(patients.createdAt));
24
+
25
+ // Get report counts for these patients
26
+ const counts = db.select({ patientId: reports.patientId }).from(reports).all();
27
+ const countsMap = counts.reduce((acc: any, curr) => {
28
+ if (curr.patientId) {
29
+ acc[curr.patientId] = (acc[curr.patientId] || 0) + 1;
30
+ }
31
+ return acc;
32
+ }, {});
33
+
34
+ const enrichedData = data.map((p) => ({
35
+ ...p,
36
+ reportCount: countsMap[p.id] || 0
37
+ }));
38
+
39
+ return NextResponse.json(enrichedData);
40
+ } catch (e: any) {
41
+ return NextResponse.json({ error: e.message }, { status: 500 });
42
+ }
43
+ }
44
+
45
+ export async function POST(req: Request) {
46
+ try {
47
+ const body = await req.json();
48
+ const newPatient = {
49
+ id: randomUUID(),
50
+ patientName: body.patientName,
51
+ patientIdNumber: body.patientIdNumber || null,
52
+ dob: body.dob || null,
53
+ age: body.age || null,
54
+ gender: body.gender || null,
55
+ contactInfo: body.contactInfo || null,
56
+ notes: body.notes || null,
57
+ createdAt: new Date().toISOString(),
58
+ updatedAt: new Date().toISOString(),
59
+ };
60
+
61
+ await db.insert(patients).values(newPatient);
62
+
63
+ return NextResponse.json(newPatient);
64
+ } catch (e: any) {
65
+ return NextResponse.json({ error: e.message }, { status: 500 });
66
+ }
67
+ }
@@ -0,0 +1,25 @@
1
+ import { db } from "@/db";
2
+ import { patients } from "@/db/schema";
3
+ import { desc, like, or } from "drizzle-orm";
4
+ import { NextResponse } from "next/server";
5
+
6
+ export async function GET(req: Request) {
7
+ try {
8
+ const url = new URL(req.url);
9
+ const q = url.searchParams.get("q");
10
+
11
+ if (!q || q.length < 2) {
12
+ return NextResponse.json([]);
13
+ }
14
+
15
+ const data = await db.select().from(patients).where(
16
+ or(
17
+ like(patients.patientName, `%${q}%`),
18
+ like(patients.patientIdNumber, `%${q}%`)
19
+ )
20
+ ).orderBy(desc(patients.createdAt)).limit(10);
21
+ return NextResponse.json(data);
22
+ } catch (e: any) {
23
+ return NextResponse.json({ error: e.message }, { status: 500 });
24
+ }
25
+ }
@@ -0,0 +1,84 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { reports } from "@/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+
6
+ // GET /api/reports/[id] — Get a single report
7
+ export async function GET(
8
+ _request: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ const { id } = await params;
12
+ try {
13
+ const row = db.select().from(reports).where(eq(reports.id, id)).get();
14
+ if (!row) {
15
+ return NextResponse.json({ error: "Report not found" }, { status: 404 });
16
+ }
17
+ const reportData = JSON.parse(row.reportData);
18
+ if (row.imageData && !reportData.image_data) {
19
+ reportData.image_data = row.imageData;
20
+ }
21
+
22
+ return NextResponse.json({
23
+ id: row.id,
24
+ patient_name: row.patientName,
25
+ modality: row.modality,
26
+ urgency: row.urgency,
27
+ report_status: row.reportStatus,
28
+ report_data: reportData,
29
+ created_at: row.createdAt,
30
+ });
31
+ } catch (error) {
32
+ console.error("[API] Error fetching report:", error);
33
+ return NextResponse.json({ error: "Failed to fetch report" }, { status: 500 });
34
+ }
35
+ }
36
+
37
+ // PUT /api/reports/[id] — Update report data
38
+ export async function PUT(
39
+ request: NextRequest,
40
+ { params }: { params: Promise<{ id: string }> }
41
+ ) {
42
+ const { id } = await params;
43
+ try {
44
+ const body = await request.json();
45
+ const { updates } = body;
46
+
47
+ // Get current report
48
+ const row = db.select().from(reports).where(eq(reports.id, id)).get();
49
+ if (!row) {
50
+ return NextResponse.json({ error: "Report not found" }, { status: 404 });
51
+ }
52
+
53
+ const currentData = JSON.parse(row.reportData);
54
+ const updatedData = { ...currentData, ...updates };
55
+
56
+ db.update(reports)
57
+ .set({
58
+ reportData: JSON.stringify(updatedData),
59
+ urgency: updatedData.urgency || row.urgency,
60
+ })
61
+ .where(eq(reports.id, id))
62
+ .run();
63
+
64
+ return NextResponse.json({ success: true });
65
+ } catch (error) {
66
+ console.error("[API] Error updating report:", error);
67
+ return NextResponse.json({ error: "Failed to update report" }, { status: 500 });
68
+ }
69
+ }
70
+
71
+ // DELETE /api/reports/[id] — Delete a report
72
+ export async function DELETE(
73
+ _request: NextRequest,
74
+ { params }: { params: Promise<{ id: string }> }
75
+ ) {
76
+ const { id } = await params;
77
+ try {
78
+ db.delete(reports).where(eq(reports.id, id)).run();
79
+ return NextResponse.json({ success: true });
80
+ } catch (error) {
81
+ console.error("[API] Error deleting report:", error);
82
+ return NextResponse.json({ error: "Failed to delete report" }, { status: 500 });
83
+ }
84
+ }
@@ -0,0 +1,87 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { reports } from "@/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+
6
+ // PATCH /api/reports/[id]/status — Update report status (approve/reject/unreject)
7
+ export async function PATCH(
8
+ request: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ const { id } = await params;
12
+ try {
13
+ const body = await request.json();
14
+ const { status, signature, rejectionReason, notes, userName, userRole } = body;
15
+
16
+ // Get current report
17
+ const row = db.select().from(reports).where(eq(reports.id, id)).get();
18
+ if (!row) {
19
+ return NextResponse.json({ error: "Report not found" }, { status: 404 });
20
+ }
21
+
22
+ const updatedData = JSON.parse(row.reportData);
23
+
24
+ // Update status in report_data
25
+ updatedData.report_footer.report_status = status;
26
+
27
+ // Collaboration: logs & comments
28
+ if (!updatedData.collaboration) {
29
+ updatedData.collaboration = { comments: [], logs: [] };
30
+ }
31
+ const timestamp = new Date().toISOString();
32
+ const user = userName || "System";
33
+
34
+ updatedData.collaboration.logs.push({
35
+ id: `log_${Date.now()}`,
36
+ action: `Status Changed to ${status}`,
37
+ user,
38
+ timestamp,
39
+ details: status === "Rejected" ? `Reason: ${rejectionReason}` :
40
+ status === "Approved" ? "Report Approved" : "Status reset",
41
+ });
42
+
43
+ if (notes || rejectionReason) {
44
+ updatedData.collaboration.comments.push({
45
+ id: `comment_${Date.now()}`,
46
+ author: user,
47
+ role: userRole || "System",
48
+ text: notes || rejectionReason || "",
49
+ timestamp,
50
+ });
51
+ }
52
+
53
+ // Handle approval
54
+ if (status === "Approved") {
55
+ updatedData.report_footer.approved_at = timestamp;
56
+ if (signature) updatedData.report_footer.signature = signature;
57
+ updatedData.report_footer.approved_by = user;
58
+ }
59
+
60
+ // Handle rejection
61
+ if (status === "Rejected" && rejectionReason) {
62
+ updatedData.report_footer.rejection_reason = rejectionReason;
63
+ }
64
+
65
+ // Handle unreject (clear rejection reason)
66
+ if (status === "Pending") {
67
+ updatedData.report_footer.rejection_reason = undefined;
68
+ }
69
+
70
+ // Update SQLite
71
+ db.update(reports)
72
+ .set({
73
+ reportData: JSON.stringify(updatedData),
74
+ reportStatus: status,
75
+ })
76
+ .where(eq(reports.id, id))
77
+ .run();
78
+
79
+ return NextResponse.json({
80
+ success: true,
81
+ report_data: updatedData,
82
+ });
83
+ } catch (error) {
84
+ console.error("[API] Error updating report status:", error);
85
+ return NextResponse.json({ error: "Failed to update status" }, { status: 500 });
86
+ }
87
+ }
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { reports, patients } from "@/db/schema";
4
+
5
+ // DELETE /api/reports/clear — Delete all reports and patients
6
+ export async function DELETE() {
7
+ try {
8
+ db.delete(reports).run();
9
+ db.delete(patients).run();
10
+ return NextResponse.json({ success: true });
11
+ } catch (error) {
12
+ console.error("[API] Error clearing reports:", error);
13
+ return NextResponse.json({ error: "Failed to clear reports" }, { status: 500 });
14
+ }
15
+ }
16
+