From 38daf9b419222d6cc2bab1c4267731160dcacaad Mon Sep 17 00:00:00 2001 From: Franklin Date: Tue, 5 Dec 2023 17:03:57 -0500 Subject: [PATCH 1/6] chore: getting documents --- apps/nextjs/package.json | 2 +- .../patient/[patientId]/page.tsx | 37 ++++++++++ packages/api/src/canvas/canvas-client.ts | 15 +++- packages/api/src/root.ts | 2 + packages/api/src/router/document.ts | 61 ++++++++++++++++ packages/api/src/validators.ts | 69 +++++++++++++++++++ 6 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 packages/api/src/router/document.ts diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index e32b4ff9..ec7e5b57 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "pnpm with-env next build", "clean": "git clean -xdf .next .turbo node_modules", - "dev": "pnpm with-env next dev", + "dev": "pnpm with-env next dev --turbo", "lint": "dotenv -v SKIP_ENV_VALIDATION=1 next lint", "format": "prettier --check . --ignore-path ../../.gitignore", "start": "pnpm with-env next start", diff --git a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx index 15377f42..1f783b58 100644 --- a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx +++ b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx @@ -39,6 +39,12 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { }, }); + const documents = await api.document.searchDocument.query({ + query: { + subject: params.patientId + } + }) + return ( @@ -105,6 +111,37 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { + + + + Documents + + + + A list of your documents. + + + Created At + Received At + Recipient + Payload + Read + + + + {documents.map((doc, i) => ( + + {doc.resource.date} + + test + + {doc.resource.content[0]?.attachment.url} + {doc.resource.date} + + ))} + +
+
); }; diff --git a/packages/api/src/canvas/canvas-client.ts b/packages/api/src/canvas/canvas-client.ts index 5c902be2..566d8488 100644 --- a/packages/api/src/canvas/canvas-client.ts +++ b/packages/api/src/canvas/canvas-client.ts @@ -4,6 +4,9 @@ import { BundleSchema, bundleSchema, careTeamSchema, + documentReferenceSchema, + entryResourceSchema, + linkSchema, ResourceSchema, } from "../validators"; @@ -1477,7 +1480,7 @@ export const get_ReadDocumentreference = { document_reference_id: z.string(), }), }), - response: z.unknown(), + response: documentReferenceSchema, }; export type get_SearchDocumentreference = typeof get_SearchDocumentreference; @@ -1494,7 +1497,13 @@ export const get_SearchDocumentreference = { category: z.string().optional(), }), }), - response: z.unknown(), + response: z.object({ + resourceType: z.literal("Bundle"), + type: z.literal("searchset"), + total: z.number(), + link: z.array(linkSchema), + entry: z.array(entryResourceSchema), + }), }; export type get_ReadEncounter = typeof get_ReadEncounter; @@ -3411,7 +3420,7 @@ type MaybeOptionalArg = RequiredKeys extends never export class ApiClient { baseUrl = ""; - constructor(public fetcher: Fetcher) {} + constructor(public fetcher: Fetcher) { } setBaseUrl(baseUrl: string) { this.baseUrl = baseUrl; diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index fef6738f..17c5093c 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -2,6 +2,7 @@ import { authRouter } from "./router/auth"; import { canvasRouter } from "./router/canvas"; import { careTeamRouter } from "./router/care-team"; import { communicationRouter } from "./router/communication"; +import { documentRouter } from "./router/document"; import { postRouter } from "./router/post"; import { practitionerRouter } from "./router/practitioner"; import { createTRPCRouter } from "./trpc"; @@ -18,6 +19,7 @@ export const appRouter = createTRPCRouter({ careTeam: careTeamRouter, practitioner: practitionerRouter, communication: communicationRouter, + document: documentRouter, }); // export type definition of API diff --git a/packages/api/src/router/document.ts b/packages/api/src/router/document.ts new file mode 100644 index 00000000..6050882e --- /dev/null +++ b/packages/api/src/router/document.ts @@ -0,0 +1,61 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { createTRPCRouter, protectedCanvasProcedure } from "../trpc"; +import { get_ReadDocumentreference, get_SearchDocumentreference } from "../canvas/canvas-client"; + +export const documentRouter = createTRPCRouter({ + getDocument: protectedCanvasProcedure.input(get_ReadDocumentreference.parameters).query(async ({ ctx, input }) => { + + const { api, canvasToken } = ctx; + + if (!canvasToken) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Canvas token is missing", + }); + } + + const data = await api.get("/DocumentReference/{document_reference_id}", { + path: { + document_reference_id: input.path.document_reference_id, + }, + }) + + console.log("DATA", data) + + return data + }), + + searchDocument: protectedCanvasProcedure.input(get_SearchDocumentreference.parameters).query(async ({ ctx, input }) => { + + try { + + const { api, canvasToken } = ctx; + + if (!canvasToken) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Canvas token is missing", + }); + } + + // @link: https://postman.com/canvasmedical/workspace/canvas-medical-public-documentation/request/17030070-e85e9dc7-3dd7-4a4f-a648-f21d291c4b59 + const data = await api.get("/DocumentReference", { + query: { + status: "current", + type: "http://loinc.org|94093-2", + subject: input.query.subject, + category: "invoicefull", + } + }) + + return data.entry + + } catch (e) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while fetching communication data", + }) + } + }), +}) diff --git a/packages/api/src/validators.ts b/packages/api/src/validators.ts index c57a0ef6..ef18d7d0 100644 --- a/packages/api/src/validators.ts +++ b/packages/api/src/validators.ts @@ -264,4 +264,73 @@ export const bundleSchema = z.object({ entry: z.array(entrySchema), }); +const categorySchema = z.object({ + coding: z.array(codingSchema), +}); + +const authorSchema = z.object({ + reference: z.string(), + type: z.string(), +}); + +const custodianSchema = z.object({ + reference: z.string(), + type: z.string(), +}); + +const attachmentSchema = z.object({ + contentType: z.string(), + url: z.string(), +}); + +const contentSchema = z.object({ + attachment: attachmentSchema, + format: codingSchema, +}); + +export const documentReferenceSchema = z.object({ + resourceType: z.literal("DocumentReference"), + id: z.string(), + status: z.literal("current"), + type: z.object({ + coding: z.array(codingSchema), + }), + category: z.array(categorySchema), + subject: subjectSchema, + date: z.string(), + author: z.array(authorSchema), + custodian: custodianSchema, + content: z.array(contentSchema), +}); + +const encounterSchema = z.object({ + reference: z.string(), + type: z.string(), +}); + +const contextSchema = z.object({ + encounter: z.array(encounterSchema), + period: periodSchema, +}); + +const documentReferenceResourceSchema = z.object({ + resourceType: z.literal("DocumentReference"), + id: z.string(), + status: z.literal("current"), + type: z.object({ + coding: z.array(codingSchema), + }), + category: z.array(categorySchema), + subject: subjectSchema, + date: z.string(), + author: z.array(authorSchema), + custodian: custodianSchema, + content: z.array(contentSchema), + context: contextSchema, +}); + +export const entryResourceSchema = z.object({ + resource: documentReferenceResourceSchema, +}); + // From d3bb595d0c4ea06133427f5b72c264c81cc35328 Mon Sep 17 00:00:00 2001 From: Franklin Date: Tue, 5 Dec 2023 21:48:23 -0500 Subject: [PATCH 2/6] wip: fixin queries for messages --- apps/nextjs/package.json | 2 +- .../patient/[patientId]/page.tsx | 54 ++++++++++--------- .../practitioner/search-practitioner.tsx | 11 ++-- packages/api/src/router/communication.ts | 2 + packages/api/src/router/document.ts | 2 +- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index ec7e5b57..e32b4ff9 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "pnpm with-env next build", "clean": "git clean -xdf .next .turbo node_modules", - "dev": "pnpm with-env next dev --turbo", + "dev": "pnpm with-env next dev", "lint": "dotenv -v SKIP_ENV_VALIDATION=1 next lint", "format": "prettier --check . --ignore-path ../../.gitignore", "start": "pnpm with-env next start", diff --git a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx index 1f783b58..b141c96c 100644 --- a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx +++ b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx @@ -29,22 +29,24 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { const listReceivedMsgs = await api.communication.searchRecipientMsgs.query({ query: { - recipient: params.patientId, + recipient: `Patient/${params.patientId}`, }, }); const listSendMsgs = await api.communication.searchSenderMsgs.query({ query: { - sender: params.patientId, + sender: `Patient/${params.patientId}`, }, }); const documents = await api.document.searchDocument.query({ query: { - subject: params.patientId + subject: `Patient/${params.patientId}` } }) + console.log("Documents: ", documents) + return ( @@ -69,16 +71,18 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { - {listReceivedMsgs.entry.map((msg, i) => ( - - {msg.resource.sent} - - {msg.resource.received ?? "No received yet"} - - {msg.resource.recipient[0]?.reference} - {msg.resource.payload[0]?.contentString} - - ))} + {listReceivedMsgs.total > 0 && ( + listReceivedMsgs.entry.map((msg, i) => ( + + {msg.resource.sent} + + {msg.resource.received ?? "No received yet"} + + {msg.resource.recipient[0]?.reference} + {msg.resource.payload[0]?.contentString} + + )) + )} @@ -94,11 +98,10 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { Received At Recipient Payload - Read - {listSendMsgs.entry.map((msg, i) => ( + {listSendMsgs.total > 0 && listSendMsgs.entry.map((msg, i) => ( {msg.resource.sent} @@ -125,20 +128,19 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { Received At Recipient Payload - Read - {documents.map((doc, i) => ( - - {doc.resource.date} - - test - - {doc.resource.content[0]?.attachment.url} - {doc.resource.date} - - ))} + {/* {documents.map((doc, i) => ( */} + {/* */} + {/* {doc.resource.date} */} + {/* */} + {/* test */} + {/* */} + {/* {doc.resource.content[0]?.attachment.url} */} + {/* {doc.resource.date} */} + {/* */} + {/* ))} */} diff --git a/apps/nextjs/src/components/practitioner/search-practitioner.tsx b/apps/nextjs/src/components/practitioner/search-practitioner.tsx index 7aedaa69..2909691f 100644 --- a/apps/nextjs/src/components/practitioner/search-practitioner.tsx +++ b/apps/nextjs/src/components/practitioner/search-practitioner.tsx @@ -25,8 +25,11 @@ const SearchPractitioner = ({ const [value, setValue] = useState(""); const { data } = api.practitioner.searchPractitioners.useQuery({ - query: {}, + query: { + name: "", + }, }); + const practitioners = data?.entry.map((entry) => entry.resource) ?? []; return ( @@ -40,9 +43,9 @@ const SearchPractitioner = ({ > {value ? practitioners?.find( - (practitioner) => - practitioner.name[0]!.text.toLowerCase() === value, - )?.name[0]!.text + (practitioner) => + practitioner.name[0]!.text.toLowerCase() === value, + )?.name[0]!.text : "Select practitioner..."} diff --git a/packages/api/src/router/communication.ts b/packages/api/src/router/communication.ts index 85d179ce..b6443d91 100644 --- a/packages/api/src/router/communication.ts +++ b/packages/api/src/router/communication.ts @@ -6,6 +6,8 @@ import { get_SearchCommunicationSender, } from "../canvas/canvas-client"; import { createTRPCRouter, protectedCanvasProcedure } from "../trpc"; +import { env } from "../env.mjs"; +import { BundleSchema } from "../validators"; export const communicationRouter = createTRPCRouter({ createMsg: protectedCanvasProcedure diff --git a/packages/api/src/router/document.ts b/packages/api/src/router/document.ts index 6050882e..85897490 100644 --- a/packages/api/src/router/document.ts +++ b/packages/api/src/router/document.ts @@ -45,7 +45,7 @@ export const documentRouter = createTRPCRouter({ status: "current", type: "http://loinc.org|94093-2", subject: input.query.subject, - category: "invoicefull", + // category: "invoicefull", } }) From b79f5096e0b7d0602094a8025ced6e938ae7b439 Mon Sep 17 00:00:00 2001 From: Franklin Date: Wed, 6 Dec 2023 12:23:02 -0500 Subject: [PATCH 3/6] chore: getting documents --- .../patient/[patientId]/page.tsx | 66 +++++++------ packages/api/src/canvas/canvas-client.ts | 14 +-- packages/api/src/router/document.ts | 96 ++++++++++--------- packages/api/src/validators.ts | 40 ++++++++ 4 files changed, 131 insertions(+), 85 deletions(-) diff --git a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx index b141c96c..8493d8bc 100644 --- a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx +++ b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx @@ -41,11 +41,11 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { const documents = await api.document.searchDocument.query({ query: { - subject: `Patient/${params.patientId}` - } - }) + subject: `Patient/${params.patientId}`, + }, + }); - console.log("Documents: ", documents) + console.log("DOCUMET", documents); return ( @@ -71,7 +71,7 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { - {listReceivedMsgs.total > 0 && ( + {listReceivedMsgs.total > 0 && listReceivedMsgs.entry.map((msg, i) => ( {msg.resource.sent} @@ -79,10 +79,11 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { {msg.resource.received ?? "No received yet"} {msg.resource.recipient[0]?.reference} - {msg.resource.payload[0]?.contentString} + + {msg.resource.payload[0]?.contentString} + - )) - )} + ))} @@ -101,24 +102,25 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { - {listSendMsgs.total > 0 && listSendMsgs.entry.map((msg, i) => ( - - {msg.resource.sent} - - {msg.resource.received ?? "No received yet"} - - {msg.resource.recipient[0]?.reference} - {msg.resource.payload[0]?.contentString} - - ))} + {listSendMsgs.total > 0 && + listSendMsgs.entry.map((msg, i) => ( + + {msg.resource.sent} + + {msg.resource.received ?? "No received yet"} + + {msg.resource.recipient[0]?.reference} + + {msg.resource.payload[0]?.contentString} + + + ))} - - Documents - + Documents A list of your documents. @@ -131,16 +133,18 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { - {/* {documents.map((doc, i) => ( */} - {/* */} - {/* {doc.resource.date} */} - {/* */} - {/* test */} - {/* */} - {/* {doc.resource.content[0]?.attachment.url} */} - {/* {doc.resource.date} */} - {/* */} - {/* ))} */} + {/* @ts-expect-error */} + {documents.total > 0 && + documents?.entry?.map((doc, i) => ( + + {doc.resource.date} + test + + {doc.resource.content[0]?.attachment.url} + + {doc.resource.date} + + ))}
diff --git a/packages/api/src/canvas/canvas-client.ts b/packages/api/src/canvas/canvas-client.ts index 2f4e8f1c..b139a24e 100644 --- a/packages/api/src/canvas/canvas-client.ts +++ b/packages/api/src/canvas/canvas-client.ts @@ -8,8 +8,8 @@ import { entryResourceSchema, linkSchema, ResourceSchema, - scheduleBundleSchema, - slotBundleSchema + scheduleBundleSchema, + slotBundleSchema, } from "../validators"; export type post_GetAnOauthToken = typeof post_GetAnOauthToken; @@ -1499,13 +1499,7 @@ export const get_SearchDocumentreference = { category: z.string().optional(), }), }), - response: z.object({ - resourceType: z.literal("Bundle"), - type: z.literal("searchset"), - total: z.number(), - link: z.array(linkSchema), - entry: z.array(entryResourceSchema), - }), + response: z.unknown(), }; export type get_ReadEncounter = typeof get_ReadEncounter; @@ -3422,7 +3416,7 @@ type MaybeOptionalArg = RequiredKeys extends never export class ApiClient { baseUrl = ""; - constructor(public fetcher: Fetcher) { } + constructor(public fetcher: Fetcher) {} setBaseUrl(baseUrl: string) { this.baseUrl = baseUrl; diff --git a/packages/api/src/router/document.ts b/packages/api/src/router/document.ts index 85897490..b1ac3d8d 100644 --- a/packages/api/src/router/document.ts +++ b/packages/api/src/router/document.ts @@ -1,61 +1,69 @@ import { TRPCError } from "@trpc/server"; -import { z } from "zod"; + +import { + get_ReadDocumentreference, + get_SearchDocumentreference, +} from "../canvas/canvas-client"; import { createTRPCRouter, protectedCanvasProcedure } from "../trpc"; -import { get_ReadDocumentreference, get_SearchDocumentreference } from "../canvas/canvas-client"; export const documentRouter = createTRPCRouter({ - getDocument: protectedCanvasProcedure.input(get_ReadDocumentreference.parameters).query(async ({ ctx, input }) => { + getDocument: protectedCanvasProcedure + .input(get_ReadDocumentreference.parameters) + .query(async ({ ctx, input }) => { + const { api, canvasToken } = ctx; - const { api, canvasToken } = ctx; + if (!canvasToken) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Canvas token is missing", + }); + } - if (!canvasToken) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Canvas token is missing", + const data = await api.get("/DocumentReference/{document_reference_id}", { + path: { + document_reference_id: input.path.document_reference_id, + }, }); - } - const data = await api.get("/DocumentReference/{document_reference_id}", { - path: { - document_reference_id: input.path.document_reference_id, - }, - }) + return data; + }), - console.log("DATA", data) + searchDocument: protectedCanvasProcedure + .input(get_SearchDocumentreference.parameters) + .query(async ({ ctx, input }) => { + try { + const { api, canvasToken } = ctx; - return data - }), + if (!canvasToken) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Canvas token is missing", + }); + } - searchDocument: protectedCanvasProcedure.input(get_SearchDocumentreference.parameters).query(async ({ ctx, input }) => { + // @link: https://postman.com/canvasmedical/workspace/canvas-medical-public-documentation/request/17030070-e85e9dc7-3dd7-4a4f-a648-f21d291c4b59 + const documentsData = await api.get("/DocumentReference", { + query: { + status: "current", + type: "http://loinc.org|94093-2", + // subject: input.query.subject, + // category: "invoicefull", + }, + }); - try { + console.log("documentsData ------>>>>", documentsData); - const { api, canvasToken } = ctx; + const validatedData = + get_SearchDocumentreference.response.parse(documentsData); - if (!canvasToken) { + console.log("validatedData", validatedData); + + return validatedData; + } catch (e) { throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Canvas token is missing", + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while fetching communication data", }); } - - // @link: https://postman.com/canvasmedical/workspace/canvas-medical-public-documentation/request/17030070-e85e9dc7-3dd7-4a4f-a648-f21d291c4b59 - const data = await api.get("/DocumentReference", { - query: { - status: "current", - type: "http://loinc.org|94093-2", - subject: input.query.subject, - // category: "invoicefull", - } - }) - - return data.entry - - } catch (e) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "An error occurred while fetching communication data", - }) - } - }), -}) + }), +}); diff --git a/packages/api/src/validators.ts b/packages/api/src/validators.ts index bcbe7f40..61a218c1 100644 --- a/packages/api/src/validators.ts +++ b/packages/api/src/validators.ts @@ -389,3 +389,43 @@ export const slotBundleSchema = z.object({ total: z.number(), entry: z.array(slotEntrySchema), }); + +// DocumentReference +const resourceSchema = z.object({ + resourceType: z.string(), + id: z.string(), + status: z.string(), + type: z.object({ + coding: z.array(codingSchema), + }), + category: z.array( + z.object({ + coding: z.array( + z.object({ + code: z.string(), + }), + ), + }), + ), + subject: subjectSchema, + date: z.string(), + author: z.array(authorSchema), + custodian: custodianSchema, + content: z.array(contentSchema), + context: z.object({ + encounter: z.array(encounterSchema), + period: periodSchema, + }), +}); + +const documentReferenceEntrySchema = z.object({ + resource: resourceSchema, +}); + +export const documentReferenceBundleSchema = z.object({ + resourceType: z.literal("Bundle"), + type: z.string(), + total: z.number(), + // link: z.array(linkSchema).optional(), + // entry: z.array(documentReferenceEntrySchema).optional(), +}); From 98a6b88e112dd15fafe4e6d9a6ef285c2a79c3ec Mon Sep 17 00:00:00 2001 From: Franklin Date: Wed, 6 Dec 2023 15:46:07 -0500 Subject: [PATCH 4/6] chore: view patient bill --- .../patient/[patientId]/page.tsx | 35 +++++++++++-------- packages/api/src/canvas/canvas-client.ts | 9 ++++- packages/api/src/router/document.ts | 10 ++---- packages/api/src/validators.ts | 6 ++-- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx index 8493d8bc..2627a715 100644 --- a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx +++ b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx @@ -1,3 +1,4 @@ +import { Button } from "@acme/ui/button"; import { Card, CardContent, @@ -16,6 +17,7 @@ import { } from "@acme/ui/table"; import CreateMessage from "~/components/communication/create-message"; +import { formatDateTime } from "~/lib/utils"; import { api } from "~/trpc/server"; export const runtime = "edge"; @@ -39,14 +41,12 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { }, }); - const documents = await api.document.searchDocument.query({ + const billDocuments = await api.document.searchBillDocument.query({ query: { subject: `Patient/${params.patientId}`, }, }); - console.log("DOCUMET", documents); - return ( @@ -120,29 +120,34 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { - Documents + Bills - A list of your documents. + A list of your bills Created At - Received At - Recipient - Payload + Bill - {/* @ts-expect-error */} - {documents.total > 0 && - documents?.entry?.map((doc, i) => ( + {/* @ts-expect-error: unable to get the zod schema correct for documentReference */} + {billDocuments.total > 0 && + // @ts-expect-error + billDocuments?.entry?.map((doc, i) => ( - {doc.resource.date} - test - {doc.resource.content[0]?.attachment.url} + {formatDateTime(new Date(doc.resource.date))}{" "} + + + + + - {doc.resource.date} ))} diff --git a/packages/api/src/canvas/canvas-client.ts b/packages/api/src/canvas/canvas-client.ts index b139a24e..e668ee68 100644 --- a/packages/api/src/canvas/canvas-client.ts +++ b/packages/api/src/canvas/canvas-client.ts @@ -4,9 +4,11 @@ import { BundleSchema, bundleSchema, careTeamSchema, + contentSchema, documentReferenceSchema, entryResourceSchema, linkSchema, + resourceSchema, ResourceSchema, scheduleBundleSchema, slotBundleSchema, @@ -1499,7 +1501,12 @@ export const get_SearchDocumentreference = { category: z.string().optional(), }), }), - response: z.unknown(), + response: z.object({ + resourceType: z.string().optional(), + type: z.string().optional(), + total: z.number().optional(), + entry: z.unknown().optional(), + }), }; export type get_ReadEncounter = typeof get_ReadEncounter; diff --git a/packages/api/src/router/document.ts b/packages/api/src/router/document.ts index b1ac3d8d..fa48b889 100644 --- a/packages/api/src/router/document.ts +++ b/packages/api/src/router/document.ts @@ -28,7 +28,7 @@ export const documentRouter = createTRPCRouter({ return data; }), - searchDocument: protectedCanvasProcedure + searchBillDocument: protectedCanvasProcedure .input(get_SearchDocumentreference.parameters) .query(async ({ ctx, input }) => { try { @@ -46,18 +46,14 @@ export const documentRouter = createTRPCRouter({ query: { status: "current", type: "http://loinc.org|94093-2", - // subject: input.query.subject, - // category: "invoicefull", + subject: input.query.subject, + category: "invoicefull", }, }); - console.log("documentsData ------>>>>", documentsData); - const validatedData = get_SearchDocumentreference.response.parse(documentsData); - console.log("validatedData", validatedData); - return validatedData; } catch (e) { throw new TRPCError({ diff --git a/packages/api/src/validators.ts b/packages/api/src/validators.ts index 61a218c1..f44722c9 100644 --- a/packages/api/src/validators.ts +++ b/packages/api/src/validators.ts @@ -283,7 +283,7 @@ const attachmentSchema = z.object({ url: z.string(), }); -const contentSchema = z.object({ +export const contentSchema = z.object({ attachment: attachmentSchema, format: codingSchema, }); @@ -391,7 +391,7 @@ export const slotBundleSchema = z.object({ }); // DocumentReference -const resourceSchema = z.object({ +export const resourceSchema = z.object({ resourceType: z.string(), id: z.string(), status: z.string(), @@ -427,5 +427,5 @@ export const documentReferenceBundleSchema = z.object({ type: z.string(), total: z.number(), // link: z.array(linkSchema).optional(), - // entry: z.array(documentReferenceEntrySchema).optional(), + entry: z.array(documentReferenceEntrySchema).optional(), }); From db7a70094c420866cfa2cd310e6cc6bc2524cbee Mon Sep 17 00:00:00 2001 From: Franklin Date: Wed, 6 Dec 2023 20:21:34 -0500 Subject: [PATCH 5/6] chore: patients can view bills and make payments --- .../patient/[patientId]/page.tsx | 41 +++++- .../patient/_components/create-payment.tsx | 119 ++++++++++++++++++ packages/api/src/canvas/canvas-client.ts | 13 +- packages/api/src/root.ts | 2 + packages/api/src/router/payment.ts | 88 +++++++++++++ .../api/src/validators/document-reference.ts | 83 ++++++++++++ packages/api/src/validators/payment.ts | 55 ++++++++ 7 files changed, 390 insertions(+), 11 deletions(-) create mode 100644 apps/nextjs/src/app/(authenticated)/patient/_components/create-payment.tsx create mode 100644 packages/api/src/router/payment.ts create mode 100644 packages/api/src/validators/document-reference.ts create mode 100644 packages/api/src/validators/payment.ts diff --git a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx index 5edea6ba..d0618b8d 100644 --- a/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx +++ b/apps/nextjs/src/app/(authenticated)/patient/[patientId]/page.tsx @@ -19,6 +19,7 @@ import { import CreateMessage from "~/components/communication/create-message"; import { formatDateTime } from "~/lib/utils"; import { api } from "~/trpc/server"; +import CreatePayment from "../_components/create-payment"; export const runtime = "edge"; @@ -47,6 +48,12 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { }, }); + const payments = await api.payment.searchPayments.query({ + query: { + request: `Patient/${params.patientId}`, + }, + }); + return ( @@ -131,17 +138,17 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => { - {/* @ts-expect-error: unable to get the zod schema correct for documentReference */} {billDocuments.total > 0 && - // @ts-expect-error billDocuments?.entry?.map((doc, i) => ( - {formatDateTime(new Date(doc.resource.date))}{" "} + {/* @ts-ignore */} + {formatDateTime(new Date(doc?.resource?.date!))}{" "} @@ -153,6 +160,32 @@ const PatientIdPage = async ({ params }: { params: { patientId: string } }) => {
+ + + Payments + + + + A list of your payments + + + Created At + Amount + + + + {payments.total > 0 && + payments.entry.map((bill, i) => ( + + + {formatDateTime(new Date(bill.resource.created))}{" "} + + {bill.resource.amount.value.toFixed(2)} + + ))} + +
+
); }; diff --git a/apps/nextjs/src/app/(authenticated)/patient/_components/create-payment.tsx b/apps/nextjs/src/app/(authenticated)/patient/_components/create-payment.tsx new file mode 100644 index 00000000..a9581da0 --- /dev/null +++ b/apps/nextjs/src/app/(authenticated)/patient/_components/create-payment.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@acme/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@acme/ui/form"; +import { Input } from "@acme/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@acme/ui/popover"; +import { toast } from "@acme/ui/use-toast"; + +import { api } from "~/trpc/react"; + +const CreatePayment = ({ reference }: { reference: string }) => { + const router = useRouter(); + + const CreatePaymentFormSchema = z.object({ + amount: z.coerce.number().gt(0, { + message: "Amount must be greater than 0", + }), + }); + + const createPayment = api.payment.createPayment.useMutation({ + onSuccess: () => { + toast({ + title: "Payment created", + description: "Payment created successfully", + }); + router.refresh(); + }, + onError: (error) => { + toast({ + title: "Payment creation failed", + description: error.message, + variant: "destructive", + }); + }, + }); + + const createPaymentForm = useForm>({ + resolver: zodResolver(CreatePaymentFormSchema), + defaultValues: { + amount: 0, + }, + }); + + const onSubmitCreatePayment = async ( + data: z.infer, + ) => { + try { + await createPayment.mutateAsync({ + body: { + resourceType: "PaymentNotice", + status: "active", + recipient: {}, + request: { + reference: reference, + }, + payment: {}, + created: new Date().toISOString(), + amount: { + value: data.amount, + currency: "USD", + }, + }, + }); + } catch (e) { + console.log(e); + } + }; + + return ( + + + + + +
+ + ( + + Amount + + + + + The amount of the payment to be made. + + + + )} + /> + + + +
+
+ ); +}; + +export default CreatePayment; diff --git a/packages/api/src/canvas/canvas-client.ts b/packages/api/src/canvas/canvas-client.ts index e668ee68..dd2b7211 100644 --- a/packages/api/src/canvas/canvas-client.ts +++ b/packages/api/src/canvas/canvas-client.ts @@ -13,6 +13,8 @@ import { scheduleBundleSchema, slotBundleSchema, } from "../validators"; +import { searchPaymentNoticeBundleSchema } from "../validators/payment"; +import { searchDocumentNoticeBundleSchema } from "../validators/document-reference"; export type post_GetAnOauthToken = typeof post_GetAnOauthToken; export const post_GetAnOauthToken = { @@ -1501,12 +1503,7 @@ export const get_SearchDocumentreference = { category: z.string().optional(), }), }), - response: z.object({ - resourceType: z.string().optional(), - type: z.string().optional(), - total: z.number().optional(), - entry: z.unknown().optional(), - }), + response: searchDocumentNoticeBundleSchema, }; export type get_ReadEncounter = typeof get_ReadEncounter; @@ -2717,7 +2714,7 @@ export const get_SearchPaymentnotice = { request: z.string().optional(), }), }), - response: z.unknown(), + response: searchPaymentNoticeBundleSchema, }; export type post_CreatePaymentnotice = typeof post_CreatePaymentnotice; @@ -2733,6 +2730,8 @@ export const post_CreatePaymentnotice = { }) .optional(), created: z.string().optional(), + payment: z.object({}), + recipient: z.object({}), request: z .object({ reference: z.string().optional(), diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index b1069716..db968de7 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -6,6 +6,7 @@ import { coverageRouter } from "./router/coverage"; import { documentRouter } from "./router/document"; import { patientRouter } from "./router/patient"; import { patientMedicalHistoryRouter } from "./router/patient-medical-history"; +import { paymentRouter } from "./router/payment"; import { postRouter } from "./router/post"; import { practitionerRouter } from "./router/practitioner"; import { questionnaireRouter } from "./router/questionnaire"; @@ -30,6 +31,7 @@ export const appRouter = createTRPCRouter({ coverage: coverageRouter, patientMedicalHistory: patientMedicalHistoryRouter, scheduling: schedulingRouter, + payment: paymentRouter, }); // export type definition of API diff --git a/packages/api/src/router/payment.ts b/packages/api/src/router/payment.ts new file mode 100644 index 00000000..f78d2727 --- /dev/null +++ b/packages/api/src/router/payment.ts @@ -0,0 +1,88 @@ +import { TRPCError } from "@trpc/server"; + +import { + get_SearchPaymentnotice, + post_CreatePaymentnotice, +} from "../canvas/canvas-client"; +import { createTRPCRouter, protectedCanvasProcedure } from "../trpc"; + +export const paymentRouter = createTRPCRouter({ + createPayment: protectedCanvasProcedure + .input(post_CreatePaymentnotice.parameters) + .mutation(async ({ ctx, input }) => { + try { + const { api, canvasToken } = ctx; + + if (!canvasToken) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Canvas token is missing", + }); + } + + const paymentData = await api.post("/PaymentNotice", { + body: { + status: "active", + request: { + reference: input.body.request?.reference, + }, + created: new Date().toISOString(), + payment: {}, + recipient: {}, + amount: { + currency: "USD", + value: input.body.amount?.value, + }, + }, + }); + + // @ts-ignore + if (paymentData?.resourceType === "OperationOutcome") { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + // @ts-ignore + message: paymentData?.issue[0]?.details.text, + }); + } + return paymentData; + } catch (e) { + if (e instanceof TRPCError) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: e.message, + }); + } + } + }), + + searchPayments: protectedCanvasProcedure + .input(get_SearchPaymentnotice.parameters) + .query(async ({ ctx, input }) => { + try { + const { api, canvasToken } = ctx; + + if (!canvasToken) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Canvas token is missing", + }); + } + + const paymentData = await api.get("/PaymentNotice", { + query: { + request: input.query.request, + }, + }); + + const validatedData = + get_SearchPaymentnotice.response.parse(paymentData); + + return validatedData; + } catch (e) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while creating payment", + }); + } + }), +}); diff --git a/packages/api/src/validators/document-reference.ts b/packages/api/src/validators/document-reference.ts new file mode 100644 index 00000000..ac0e1d46 --- /dev/null +++ b/packages/api/src/validators/document-reference.ts @@ -0,0 +1,83 @@ +import { z } from "zod"; + +const codingSchema = z.object({ + system: z.string().optional(), + code: z.string(), + display: z.string().optional(), +}); + +const linkSchema = z.object({ + relation: z.string(), + url: z.string(), +}); + +const subjectSchema = z.object({ + reference: z.string(), + type: z.string(), +}); + +const periodSchema = z.object({ + start: z.string(), +}); + +const encounterSchema = z.object({ + reference: z.string(), + type: z.string(), +}); + +const attachmentSchema = z.object({ + contentType: z.string(), + url: z.string(), +}); + +const formatSchema = z.object({ + system: z.string(), + code: z.string(), + display: z.string(), +}); + +const contentSchema = z.object({ + attachment: attachmentSchema, + format: formatSchema, +}); + +const resourceSchema = z.object({ + resourceType: z.string().optional(), + id: z.string().optional(), + status: z.string().optional(), + type: z + .object({ + coding: z.array(codingSchema), + }) + .optional(), + category: z + .array( + z.object({ + coding: z.array(codingSchema), + }), + ) + .optional(), + subject: subjectSchema, + date: z.string().optional(), + author: z.array(subjectSchema).optional(), + custodian: subjectSchema, + content: z.array(contentSchema).optional(), + context: z + .object({ + encounter: z.array(encounterSchema), + period: periodSchema, + }) + .optional(), +}); + +const entrySchema = z.object({ + resource: resourceSchema, +}); + +export const searchDocumentNoticeBundleSchema = z.object({ + resourceType: z.literal("Bundle"), + type: z.string(), + total: z.number(), + link: z.array(linkSchema), + entry: z.array(entrySchema), +}); diff --git a/packages/api/src/validators/payment.ts b/packages/api/src/validators/payment.ts new file mode 100644 index 00000000..e7f8a715 --- /dev/null +++ b/packages/api/src/validators/payment.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +const codingSchema = z.object({ + system: z.string(), + code: z.string(), +}); + +const paymentStatusSchema = z.object({ + coding: z.array(codingSchema), +}); + +const amountSchema = z.object({ + value: z.number(), + currency: z.string(), +}); + +const resourceSchema = z.object({ + resourceType: z.string(), + id: z.string(), + status: z.string(), + request: z.object({ + reference: z.string(), + type: z.string(), + }), + created: z.string(), + payment: z.object({ + display: z.string(), + }), + recipient: z.object({ + display: z.string(), + }), + amount: amountSchema, + paymentStatus: paymentStatusSchema, +}); + +const linkSchema = z.object({ + relation: z.string(), + url: z.string(), +}); + +export const searchPaymentNoticeBundleSchema = z.object({ + resourceType: z.literal("Bundle"), + type: z.string(), + total: z.number(), + link: z.array(linkSchema), + entry: z.array( + z.object({ + resource: resourceSchema, + }), + ), +}); + +// Usage example +// const jsonResponse = /* the provided JSON response */ +// export const parsedPaymentNotice = bundleSchema.parse(jsonResponse); From 4f806e7f28604930edea7a2d3e22e8e431277af1 Mon Sep 17 00:00:00 2001 From: Franklin Date: Wed, 6 Dec 2023 20:26:19 -0500 Subject: [PATCH 6/6] chore: refresh payment form --- .../app/(authenticated)/patient/_components/create-payment.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/nextjs/src/app/(authenticated)/patient/_components/create-payment.tsx b/apps/nextjs/src/app/(authenticated)/patient/_components/create-payment.tsx index a9581da0..6e38c4f4 100644 --- a/apps/nextjs/src/app/(authenticated)/patient/_components/create-payment.tsx +++ b/apps/nextjs/src/app/(authenticated)/patient/_components/create-payment.tsx @@ -74,6 +74,8 @@ const CreatePayment = ({ reference }: { reference: string }) => { }, }, }); + + createPaymentForm.reset(); } catch (e) { console.log(e); }