@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.
- package/README.md +438 -0
- package/app/api/ai-config/route.ts +131 -0
- package/app/api/ai-config/test/route.ts +49 -0
- package/app/api/auth/auto-login/route.ts +66 -0
- package/app/api/auth/check/route.ts +17 -0
- package/app/api/auth/login/route.ts +72 -0
- package/app/api/auth/logout/route.ts +25 -0
- package/app/api/auth/me/route.ts +75 -0
- package/app/api/auth/password/route.ts +49 -0
- package/app/api/auth/setup/route.ts +63 -0
- package/app/api/auth/users/route.ts +100 -0
- package/app/api/auth/wipe/route.ts +27 -0
- package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
- package/app/api/compliance/audit/route.ts +110 -0
- package/app/api/compliance/export/patient/[id]/route.ts +108 -0
- package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
- package/app/api/compliance/settings/route.ts +93 -0
- package/app/api/copilot/annotate/route.ts +94 -0
- package/app/api/copilot/chat/route.ts +238 -0
- package/app/api/copilot/history/route.ts +95 -0
- package/app/api/copilot/reports/route.ts +81 -0
- package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
- package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
- package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
- package/app/api/fhir/Patient/[id]/route.ts +26 -0
- package/app/api/fhir/ServiceRequest/route.ts +85 -0
- package/app/api/fhir/config/route.ts +102 -0
- package/app/api/fhir/config/test-connection/route.ts +49 -0
- package/app/api/fhir/metadata/route.ts +51 -0
- package/app/api/pacs/metadata/route.ts +32 -0
- package/app/api/pacs/qido/instances/route.ts +39 -0
- package/app/api/pacs/qido/series/route.ts +38 -0
- package/app/api/pacs/qido/studies/route.ts +37 -0
- package/app/api/pacs/test/route.ts +30 -0
- package/app/api/pacs/wado/render/route.ts +51 -0
- package/app/api/patients/[id]/reports/route.ts +18 -0
- package/app/api/patients/[id]/route.ts +43 -0
- package/app/api/patients/merge/route.ts +57 -0
- package/app/api/patients/route.ts +67 -0
- package/app/api/patients/search/route.ts +25 -0
- package/app/api/reports/[id]/route.ts +84 -0
- package/app/api/reports/[id]/status/route.ts +87 -0
- package/app/api/reports/clear/route.ts +16 -0
- package/app/api/reports/route.ts +112 -0
- package/app/api/segmentation-config/route.ts +238 -0
- package/app/api/settings/route.ts +245 -0
- package/app/api/settings/test-supabase/route.ts +103 -0
- package/app/api/upload/route.ts +48 -0
- package/app/copilot/page.tsx +30 -0
- package/app/globals.css +141 -0
- package/app/history/page.tsx +242 -0
- package/app/icon.svg +3 -0
- package/app/layout.tsx +47 -0
- package/app/login/page.tsx +175 -0
- package/app/pacs/page.tsx +78 -0
- package/app/page.tsx +125 -0
- package/app/patients/[id]/page.tsx +315 -0
- package/app/patients/page.tsx +110 -0
- package/app/profile/page.tsx +208 -0
- package/app/reports/page.tsx +432 -0
- package/app/settings/page.tsx +454 -0
- package/app/setup/page.tsx +199 -0
- package/components/admin/AuditLogTable.tsx +293 -0
- package/components/copilot/ActivityIndicator.tsx +215 -0
- package/components/copilot/ChatHistoryPanel.tsx +140 -0
- package/components/copilot/ChatMessage.tsx +251 -0
- package/components/copilot/ClickableReference.tsx +40 -0
- package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
- package/components/copilot/CopilotPanel.tsx +311 -0
- package/components/copilot/FindingsList.tsx +75 -0
- package/components/copilot/ViewerPanel.tsx +460 -0
- package/components/copilot/WorkspaceLayout.tsx +398 -0
- package/components/dashboard/AIConfigPanel.tsx +339 -0
- package/components/dashboard/AppearancePanel.tsx +491 -0
- package/components/dashboard/ApprovalModal.tsx +163 -0
- package/components/dashboard/CollaborationPanel.tsx +134 -0
- package/components/dashboard/CopilotConfigPanel.tsx +337 -0
- package/components/dashboard/DicomViewer.tsx +645 -0
- package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
- package/components/dashboard/FullReportOverlay.tsx +269 -0
- package/components/dashboard/ImageViewer.tsx +541 -0
- package/components/dashboard/PatientForm.tsx +597 -0
- package/components/dashboard/RejectionModal.tsx +74 -0
- package/components/dashboard/ReportEditor.tsx +160 -0
- package/components/dashboard/ReportTemplates.tsx +729 -0
- package/components/dashboard/ReportView.tsx +539 -0
- package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
- package/components/dashboard/StudyPlaceholder.tsx +17 -0
- package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
- package/components/dashboard/UserManagementPanel.tsx +272 -0
- package/components/layout/ClientLayout.tsx +39 -0
- package/components/layout/Header.tsx +20 -0
- package/components/layout/Sidebar.tsx +119 -0
- package/components/pacs/PacsImageViewerModal.tsx +121 -0
- package/components/pacs/PacsSearchFilters.tsx +117 -0
- package/components/pacs/PacsSeriesViewer.tsx +190 -0
- package/components/pacs/PacsStudyTable.tsx +113 -0
- package/components/patients/patient-card.tsx +117 -0
- package/components/patients/patient-header.tsx +122 -0
- package/components/patients/patient-search.tsx +137 -0
- package/components/patients/patient-timeline.tsx +153 -0
- package/components/settings/ComplianceSettingsPanel.tsx +278 -0
- package/components/settings/SecurityPanel.tsx +418 -0
- package/components/ui/badge.tsx +19 -0
- package/components/ui/basic.tsx +156 -0
- package/db/index.ts +350 -0
- package/db/migrations/0000_odd_quasimodo.sql +117 -0
- package/db/migrations/meta/0000_snapshot.json +778 -0
- package/db/migrations/meta/_journal.json +13 -0
- package/db/schema.ts +239 -0
- package/drizzle.config.ts +10 -0
- package/lib/api.ts +689 -0
- package/lib/auth.ts +22 -0
- package/lib/copilot/action-executor.ts +94 -0
- package/lib/copilot/action-types.ts +72 -0
- package/lib/copilot/coordinate-mapper.ts +84 -0
- package/lib/dicomImageExtractor.ts +103 -0
- package/lib/dicomMetadataParser.ts +111 -0
- package/lib/fhir/client.ts +25 -0
- package/lib/fhir/constants.ts +21 -0
- package/lib/fhir/diagnostic-report.ts +88 -0
- package/lib/fhir/helpers.ts +73 -0
- package/lib/fhir/imaging-study.ts +49 -0
- package/lib/fhir/patient.ts +55 -0
- package/lib/fhir/service-request.ts +85 -0
- package/lib/fhir.ts +6 -0
- package/lib/pacs/dicom-utils.ts +72 -0
- package/lib/pacs/dicomweb.ts +72 -0
- package/lib/pacs/server-utils.ts +37 -0
- package/lib/patients.ts +25 -0
- package/lib/pdfHelper.ts +119 -0
- package/lib/reportHtmlGenerator.ts +581 -0
- package/lib/security/audit.ts +180 -0
- package/lib/security/authz.ts +246 -0
- package/lib/security/phi-redaction.ts +156 -0
- package/lib/security/rate-limit.ts +106 -0
- package/lib/security/secrets.ts +179 -0
- package/lib/supabase.ts +72 -0
- package/lib/utils.ts +6 -0
- package/next.config.ts +35 -0
- package/package.json +76 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +8 -0
- package/public/next.svg +1 -0
- package/public/omnirad-favicon.svg +8 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
- package/types/copilot-viewer.ts +155 -0
- package/types/copilot.ts +105 -0
- package/types/fhir.ts +21 -0
- package/types/html2pdf.d.ts +20 -0
- package/types/index.ts +139 -0
- 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
|
+
|