diff --git a/README.md b/README.md index 19ad5a2..71831bc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,43 @@ The backend consists of multiple HTTP GET, POST, PATCH, or DELETE routes, most o The frontend displays pending, approved, and picked up proprietary doctor orders at [http://localhost:5050/DoctorOrders](http://localhost:5050/DoctorOrders). While there is a login page at [http://localhost:5050](http://localhost:5050) for a dummy user, there is no user authentication or authorization system. +## Environment Variables + +The PIMS system uses environment variables to configure both the frontend and backend services. + +### Frontend Environment Variables + +The frontend environment variables are configured in `frontend/.env`: + +| Variable Name | Default Value | Description | +| ------------- | ------------- | ----------- | +| PORT | `5050` | The port that the frontend server runs on. Change if there are conflicts with port usage. | +| REACT_APP_PIMS_BACKEND_PORT | `5051` | The port that the backend server runs on. Must match the backend's `BACKEND_PORT` setting. | + +To override defaults, either: +- Start the app with environment variables: `PORT=5050 npm start` +- Create a `frontend/.env.local` file with the desired values + +### Backend Environment Variables + +The backend environment variables are configured in `backend/env.json`: + +| Variable Name | Default Value | Description | +| ------------- | ------------- | ----------- | +| BACKEND_PORT | `5051` | The port that the backend server runs on. Change if there are conflicts with port usage. | +| ALLOWED_ORIGIN | `*` | CORS allowed origins. Specify domains that are allowed to access the backend API. | +| MONGO_USERNAME | `pims-user` | Username for MongoDB authentication. Should match the user created during MongoDB setup. | +| MONGO_PASSWORD | `pims-pass` | Password for MongoDB authentication. Should match the password created during MongoDB setup. | +| MONGO_URL | `mongodb://localhost:27017/pims` | MongoDB connection URL. Update if using a different host, port, or database name. | +| AUTH_SOURCE | `pims` | MongoDB authentication source database name. | +| HTTPS_KEY_PATH | `server.key` | Path to the HTTPS private key file. Required only if `USE_HTTPS` is true. | +| HTTPS_CERT_PATH | `server.cert` | Path to the HTTPS certificate file. Required only if `USE_HTTPS` is true. | +| USE_HTTPS | `false` | Set to `true` to enable HTTPS. Ensure valid certificate and key paths are configured. | +| EHR_RXFILL_URL | `http://localhost:8080/test-ehr/ncpdp/script` | URL endpoint for sending RxFill messages to the EHR system. | +| USE_INTERMEDIARY | `true` | Set to `true` to route ETASU checks through the REMS intermediary instead of directly to REMS admin. | +| INTERMEDIARY_FHIR_URL | `http://localhost:3003/4_0_0` | Base URL of the REMS intermediary FHIR server. Used when `USE_INTERMEDIARY` is true. | +| REMS_ADMIN_NCPDP | `http://localhost:8090/ncpdp/script` | URL endpoint for sending NCPDP Script messages directly to REMS admin. | + ## Setup For an initial setup run `npm install` in both the frontend and backend subdirectories. This will install the dependencies required for each of the services. diff --git a/backend/env.json b/backend/env.json index 30e25f1..18462d2 100644 --- a/backend/env.json +++ b/backend/env.json @@ -54,5 +54,13 @@ "REMS_ADMIN_NCPDP": { "type": "string", "default": "http://localhost:8090/ncpdp/script" + }, + "INTERMEDIARY_URL": { + "type": "string", + "default": "http://localhost:8090/ncpdp/script" + }, + "EHR_NCPDP_URL": { + "type": "string", + "default": "|| 'http://localhost:8080/ncpdp/script'" } } diff --git a/backend/src/database/schemas/doctorOrderSchemas.js b/backend/src/database/schemas/doctorOrderSchemas.js index aba505c..c0cd577 100644 --- a/backend/src/database/schemas/doctorOrderSchemas.js +++ b/backend/src/database/schemas/doctorOrderSchemas.js @@ -25,6 +25,10 @@ export const orderSchema = new mongoose.Schema({ total: Number, pickupDate: String, dispenseStatus: String, + authorizationNumber: String, + authorizationExpiration: String, + denialReasonCode: String, + remsNote: String, metRequirements: [ { name: String, @@ -44,4 +48,4 @@ export const orderSchema = new mongoose.Schema({ // Compound index is used to prevent duplicates based off of the given parameters orderSchema.index({ simpleDrugName: 1, patientName: 1 }, { unique: true }); // schema level -export const doctorOrder = mongoose.model('doctorOrder', orderSchema); +export const doctorOrder = mongoose.model('doctorOrder', orderSchema); \ No newline at end of file diff --git a/backend/src/lib/pharmacyConfig.js b/backend/src/lib/pharmacyConfig.js new file mode 100644 index 0000000..dd3fbd7 --- /dev/null +++ b/backend/src/lib/pharmacyConfig.js @@ -0,0 +1,56 @@ + +// Configuration state +let config = { + useIntermediary: process.env.USE_INTERMEDIARY, + intermediaryUrl: process.env.INTERMEDIARY_URL, + remsAdminUrl: process.env.REMS_ADMIN_NCPDP, + ehrUrl: process.env.EHR_NCPDP_URL +}; + + + +export function getConfig() { + return { ...config }; +} + + +export function updateConfig(newConfig) { + config = { ...config, ...newConfig }; + console.log('Configuration updated:', config); + return { ...config }; +} + +/** + * Get the endpoint for NCPDP messages (REMS) + */ +export function getNCPDPEndpoint() { + if (config.useIntermediary) { + return `${config.intermediaryUrl}/ncpdp/script`; + } + return config.remsAdminUrl; +} + +/** + * Get the endpoint for ETASU requests + */ +export function getETASUEndpoint() { + if (config.useIntermediary) { + return `${config.intermediaryUrl}/etasu`; + } + // Direct ETASU endpoint to REMS Admin + return config.remsAdminUrl.replace('/ncpdp', '/4_0_0/GuidanceResponse/$rems-etasu'); +} + +/** + * Get the endpoint for RxFill messages (to EHR) + * RxFill is sent to both EHR and REMS Admin + * If using intermediary, send to intermediary (it forwards to both) + * If not using intermediary, return EHR endpoint (caller must also send to REMS) + */ +export function getRxFillEndpoint() { + if (config.useIntermediary) { + // Intermediary handles forwarding to both EHR and REMS Admin + return `${config.intermediaryUrl}/ncpdp/script`; + } + return config.ehrUrl; +} \ No newline at end of file diff --git a/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js b/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js index a642710..fbd3953 100644 --- a/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js +++ b/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js @@ -1,4 +1,4 @@ -/* NCPDP SCRIPT v2017071 Support */ +/* NCPDP SCRIPT v2017071 Support - Enhanced for Full REMS Compliance */ import { XMLBuilder } from 'fast-xml-parser'; import { v4 as uuidv4 } from 'uuid'; @@ -12,6 +12,9 @@ const XML_BUILDER_OPTIONS = { oneListGroup: 'true' }; +/** + * Build base NCPDP message structure + */ function buildMessage(inputMessage, body) { const { Message } = inputMessage; const { Header, Body } = Message; @@ -33,8 +36,10 @@ function buildMessage(inputMessage, body) { } }, { - Message: - 'NewRx Request Received For: ' + Body.NewRx.MedicationPrescribed.DrugDescription + MessageID: Header.MessageID + }, + { + Message: 'NewRx Request Received For: ' + Body.NewRx.MedicationPrescribed.DrugDescription }, { RelatesToMessageID: Header.MessageID }, { SentTime: time.toISOString() }, @@ -49,12 +54,15 @@ function buildMessage(inputMessage, body) { return message; } +/** + * Build NCPDP Status message (success response) + */ export function buildRxStatus(newRxMessageConvertedToJSON) { const body = [ { Status: [ { - Code: '000' // Placeholder: This is dependent on individual pharmacy + Code: '000' } ] } @@ -64,13 +72,16 @@ export function buildRxStatus(newRxMessageConvertedToJSON) { return builder.build(rxStatus); } +/** + * Build NCPDP Error message + */ export function buildRxError(newRxMessageConvertedToJSON, errorMessage) { const body = [ { Error: [ { - Code: 900, // Transaction was rejected - DescriptionCode: 1000, // Unable to identify based on information submitted + Code: 900, + DescriptionCode: 1000, Description: errorMessage } ] @@ -81,13 +92,56 @@ export function buildRxError(newRxMessageConvertedToJSON, errorMessage) { return builder.build(rxStatus); } +/** + * Build NCPDP RxFill message + * Per NCPDP spec: Sent when medication is dispensed/picked up + * Must be sent to both EHR and REMS Admin for REMS drugs + */ export const buildRxFill = newRx => { const { Message } = JSON.parse(newRx.serializedJSON); const { Header, Body } = Message; - console.log('Message', Message); + console.log('Building RxFill per NCPDP SCRIPT'); const time = new Date(); + + // Extract medication data from NewRx + const medicationPrescribed = Body.NewRx.MedicationPrescribed; + const drugCoded = medicationPrescribed.DrugCoded; + + const medicationDispensed = { + DrugDescription: medicationPrescribed.DrugDescription, + DrugCoded: { + ProductCode: drugCoded.ProductCode ? { + Code: drugCoded.ProductCode.Code, + Qualifier: drugCoded.ProductCode.Qualifier + } : undefined, + Strength: drugCoded.Strength ? { + StrengthValue: drugCoded.Strength.StrengthValue, + StrengthForm: drugCoded.Strength.StrengthForm, + StrengthUnitOfMeasure: drugCoded.Strength.StrengthUnitOfMeasure + } : undefined + }, + Quantity: { + Value: medicationPrescribed.Quantity.Value, + CodeListQualifier: medicationPrescribed.Quantity.CodeListQualifier || '87', + QuantityUnitOfMeasure: medicationPrescribed.Quantity.QuantityUnitOfMeasure + }, + DaysSupply: medicationPrescribed.DaysSupply, + WrittenDate: medicationPrescribed.WrittenDate, + Substitutions: medicationPrescribed.Substitutions?.Substitutions || + medicationPrescribed.Substitutions || '0', + NumberOfRefills: medicationPrescribed.Refills?.Quantity || + medicationPrescribed.NumberOfRefills || 0, + Sig: medicationPrescribed.Sig + }; + const message = { Message: { + '@@DatatypesVersion': '20170715', + '@@TransportVersion': '20170715', + '@@TransactionDomain': 'SCRIPT', + '@@TransactionVersion': '20170715', + '@@StructuresVersion': '20170715', + '@@ECLVersion': '20170715', Header: [ { To: { @@ -104,6 +158,7 @@ export const buildRxFill = newRx => { { MessageID: uuidv4() }, { RelatesToMessageID: Header.MessageID }, { SentTime: time.toISOString() }, + { RxReferenceNumber: Header.MessageID }, { PrescriberOrderNumber: Header.PrescriberOrderNumber } ], Body: [ @@ -117,16 +172,16 @@ export const buildRxFill = newRx => { Patient: Body.NewRx.Patient, Pharmacy: { Identification: { - NCPDPID: MOCK_VALUE, + NCPDPID: Header.To._ || MOCK_VALUE, NPI: MOCK_VALUE }, - BusinessName: Header.To._, + BusinessName: Header.To._ || 'Pharmacy', Address: { AddressLine1: MOCK_VALUE, City: MOCK_VALUE, StateProvince: MOCK_VALUE, PostalCode: MOCK_VALUE, - Country: MOCK_VALUE + CountryCode: 'US' }, CommunicationNumbers: { PrimaryTelephone: { @@ -135,12 +190,194 @@ export const buildRxFill = newRx => { } }, Prescriber: Body.NewRx.Prescriber, + MedicationDispensed: medicationDispensed + } + } + ] + } + }; + const builder = new XMLBuilder(XML_BUILDER_OPTIONS); + return builder.build(message); +}; + +/** + * Build NCPDP REMSInitiationRequest + */ +export const buildREMSInitiationRequest = newRx => { + const { Message } = JSON.parse(newRx.serializedJSON); + const { Header, Body } = Message; + const time = new Date(); + + // Extract NDC from medication (prioritize NDC, fallback to other codes) + const drugCoded = Body.NewRx.MedicationPrescribed.DrugCoded; + const ndcCode = + drugCoded?.NDC || drugCoded?.ProductCode?.Code; + const humanPatient = Body.NewRx.Patient.HumanPatient; + const patient = { + HumanPatient: { + Identification: {}, + Names: humanPatient.Names, + GenderAndSex: humanPatient.GenderAndSex, + DateOfBirth: humanPatient.DateOfBirth, + Address: humanPatient.Address + } + }; + + const message = { + Message: { + '@@DatatypesVersion': '2024011', + '@@TransportVersion': '2024011', + '@@TransactionDomain': 'SCRIPT', + '@@TransactionVersion': '2024011', + '@@StructuresVersion': '2024011', + '@@ECLVersion': '2024011', + Header: [ + { + To: { + '#text': ndcCode, + '@@Qualifier': 'ZZZ' + } + }, + { + From: { + '#text': Header.To._ || 'PIMS Pharmacy', + '@@Qualifier': 'REMS' + } + }, + { MessageID: uuidv4() }, + { SentTime: time.toISOString() }, + { + Security: { + Sender: { + SecondaryIdentification: 'PASSWORDR' + } + } + }, + { + SenderSoftware: { + SenderSoftwareDeveloper: 'PIMS', + SenderSoftwareProduct: 'PharmacySystem', + SenderSoftwareVersionRelease: '1' + } + }, + { TestMessage: 'false' } + ], + Body: [ + { + REMSInitiationRequest: { + REMSReferenceID: uuidv4().replace(/-/g, '').substring(0, 25), + Patient: patient, + Pharmacy: { + Identification: { + NCPDPID: Header.To._ || MOCK_VALUE, + NPI: MOCK_VALUE + }, + BusinessName: Header.To._ || 'PIMS Pharmacy', + CommunicationNumbers: { + PrimaryTelephone: { + Number: MOCK_VALUE + } + } + }, + Prescriber: Body.NewRx.Prescriber, MedicationPrescribed: Body.NewRx.MedicationPrescribed } } ] } }; + const builder = new XMLBuilder(XML_BUILDER_OPTIONS); return builder.build(message); }; + +/** + * Build NCPDP REMSRequest + */ +export const buildREMSRequest = (newRx, caseNumber) => { + const { Message } = JSON.parse(newRx.serializedJSON); + const { Header, Body } = Message; + const time = new Date(); + const deadlineDate = new Date(); + deadlineDate.setDate(deadlineDate.getDate() + 7); + + // Extract NDC from medication + const drugCoded = Body.NewRx.MedicationPrescribed.DrugCoded; + const ndcCode = + drugCoded?.NDC || drugCoded?.ProductCode?.Code || '66215050130'; + + const message = { + Message: { + '@@DatatypesVersion': '2024011', + '@@TransportVersion': '2024011', + '@@TransactionDomain': 'SCRIPT', + '@@TransactionVersion': '2024011', + '@@StructuresVersion': '2024011', + '@@ECLVersion': '2024011', + Header: [ + { + To: { + '#text': ndcCode, + '@@Qualifier': 'ZZZ' + } + }, + { + From: { + '#text': Header.To._ || 'PIMS Pharmacy', + '@@Qualifier': 'REMS' + } + }, + { MessageID: uuidv4() }, + { SentTime: time.toISOString() }, + { + Security: { + Sender: { + SecondaryIdentification: 'PASSWORD' + } + } + }, + { + SenderSoftware: { + SenderSoftwareDeveloper: 'PIMS', + SenderSoftwareProduct: 'PharmacySystem', + SenderSoftwareVersionRelease: '1' + } + }, + { TestMessage: 'false' } + ], + Body: [ + { + REMSRequest: { + REMSReferenceID: uuidv4().replace(/-/g, '').substring(0, 25), + Patient: Body.NewRx.Patient, + Pharmacy: { + Identification: { + NCPDPID: Header.To._ || MOCK_VALUE, + NPI: MOCK_VALUE + }, + BusinessName: Header.To._ || 'PIMS Pharmacy', + CommunicationNumbers: { + PrimaryTelephone: { + Number: MOCK_VALUE + } + } + }, + Prescriber: Body.NewRx.Prescriber, + MedicationPrescribed: Body.NewRx.MedicationPrescribed, + Request: { + SolicitedModel: { + REMSCaseID: caseNumber, + DeadlineForReply: { + Date: deadlineDate.toISOString().split('T')[0] + } + } + } + } + } + ] + } + }; + + const builder = new XMLBuilder(XML_BUILDER_OPTIONS); + return builder.build(message); +}; \ No newline at end of file diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 9af2b78..cd23700 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -5,26 +5,38 @@ import axios from 'axios'; // XML Parsing Middleware used for NCPDP SCRIPT import bodyParser from 'body-parser'; import bpx from 'body-parser-xml'; +import { parseStringPromise } from "xml2js"; import env from 'var'; import { buildRxStatus, buildRxFill, - buildRxError + buildRxError, + buildREMSInitiationRequest, + buildREMSRequest } from '../ncpdpScriptBuilder/buildScript.v2017071.js'; import { NewRx } from '../database/schemas/newRx.js'; import { medicationRequestToRemsAdmins } from '../database/data.js'; +import { getConfig, updateConfig, getNCPDPEndpoint, getETASUEndpoint, getRxFillEndpoint } from '../lib/pharmacyConfig.js'; bpx(bodyParser); router.use( bodyParser.xml({ + type: ['application/xml'], xmlParseOptions: { - normalize: true, // Trim whitespace inside text nodes - explicitArray: false // Only put nodes in array if >1 + normalize: true, + explicitArray: false } }) ); router.use(bodyParser.urlencoded({ extended: false })); +const XML2JS_OPTS = { + explicitArray: false, + trim: true, + normalize: true, + normalizeTags: true, // <-- makes all tag names lower case +}; + /** * Route: 'doctorOrders/api/getRx/pending' * Description: 'Returns all pending documents in database for PIMS' @@ -59,7 +71,6 @@ router.get('/api/getRx/pickedUp', async (_req, res) => { * Description: Process addRx / NewRx NCPDP message. */ export async function processNewRx(newRxMessageConvertedToJSON) { - console.log('processNewRx NCPDP SCRIPT message'); const newOrder = await parseNCPDPScript(newRxMessageConvertedToJSON); try { @@ -84,7 +95,43 @@ export async function processNewRx(newRxMessageConvertedToJSON) { return buildRxError(errorStr); } - return buildRxStatus(newRxMessageConvertedToJSON); + const rxStatus = buildRxStatus(newRxMessageConvertedToJSON); + console.log('Returning RxStatus'); + console.log(rxStatus); + + // If REMS drug, send REMSInitiationRequest per NCPDP spec + if (isRemsDrug(newOrder)) { + console.log('REMS drug detected - sending REMSInitiationRequest per NCPDP workflow'); + try { + const initiationResponse = await sendREMSInitiationRequest(newOrder); + + if (initiationResponse) { + const updateData = { + remsNote: initiationResponse.remsNote + }; + + if (initiationResponse.caseNumber) { + updateData.caseNumber = initiationResponse.caseNumber; + console.log('Received REMS Case Number:', initiationResponse.caseNumber); + } + + if (initiationResponse.remsPatientId) { + console.log('Received REMS Patient ID:', initiationResponse.remsPatientId); + } + + if (initiationResponse.status === 'CLOSED') { + updateData.denialReasonCode = initiationResponse.reasonCode; + console.log('REMSInitiation CLOSED:', initiationResponse.reasonCode); + } + + await doctorOrder.updateOne({ _id: newOrder._id }, updateData); + console.log('Updated order with REMSInitiation response'); + } + } catch (error) { + console.log('Error processing REMSInitiationRequest:', error); + } + } + return rxStatus; } /** @@ -94,6 +141,8 @@ export async function processNewRx(newRxMessageConvertedToJSON) { router.post('/api/addRx', async (req, res) => { // Parsing incoming NCPDP SCRIPT XML to doctorOrder JSON const newRxMessageConvertedToJSON = req.body; + console.log('processNewRx NCPDP SCRIPT message'); + console.log(JSON.stringify(req.body)); const status = await processNewRx(newRxMessageConvertedToJSON); res.send(status); console.log('Sent Status/Error'); @@ -101,28 +150,65 @@ router.post('/api/addRx', async (req, res) => { /** * Route: 'doctorOrders/api/updateRx/:id' - * Description : 'Updates prescription based on mongo id, used in etasu' + * Description : 'Updates prescription based on mongo id, sends NCPDP REMSRequest for authorization' */ router.patch('/api/updateRx/:id', async (req, res) => { try { - // Finding by id const order = await doctorOrder.findById(req.params.id).exec(); console.log('Found doctor order by id! --- ', order); - const guidanceResponse = await getGuidanceResponse(order); - const metRequirements = - guidanceResponse?.contained?.[0]?.['parameter'] || order.metRequirements; - const dispenseStatus = getDispenseStatus(order, guidanceResponse); + // Non-REMS drugs auto-approve + if (!isRemsDrug(order)) { + const newOrder = await doctorOrder.findOneAndUpdate( + { _id: req.params.id }, + { dispenseStatus: 'Approved' }, + { new: true } + ); + res.send(newOrder); + console.log('Non-REMS drug - auto-approved'); + return; + } + + // REMS drugs - send NCPDP REMSRequest per spec + console.log('REMS drug - sending REMSRequest for authorization per NCPDP workflow'); + const ncpdpResponse = await sendREMSRequest(order); + + if (!ncpdpResponse) { + res.send(order); + console.log('NCPDP REMSRequest failed'); + return; + } + + // Update based on NCPDP response + const updateData = { + dispenseStatus: getDispenseStatus(order, ncpdpResponse) + }; + + if (ncpdpResponse.status === 'APPROVED') { + updateData.authorizationNumber = ncpdpResponse.authorizationNumber; + updateData.authorizationExpiration = ncpdpResponse.authorizationExpiration; + updateData.caseNumber = ncpdpResponse.caseId; + + // Format approval note with ETASU summary + let approvalNote = `APPROVED - Authorization: ${ncpdpResponse.authorizationNumber}, Expires: ${ncpdpResponse.authorizationExpiration}`; + updateData.remsNote = approvalNote; + updateData.denialReasonCode = null; + console.log('APPROVED:', ncpdpResponse.authorizationNumber); + } else if (ncpdpResponse.status === 'DENIED') { + updateData.denialReasonCode = ncpdpResponse.reasonCode; + updateData.remsNote = ncpdpResponse.remsNote; + updateData.caseNumber = ncpdpResponse.caseId; + console.log('DENIED:', ncpdpResponse.reasonCode); + } - // Saving and updating const newOrder = await doctorOrder.findOneAndUpdate( { _id: req.params.id }, - { dispenseStatus, metRequirements }, + updateData, { new: true } ); res.send(newOrder); - console.log('Updated order'); + console.log('Updated order with NCPDP response'); } catch (error) { console.log('Error', error); return error; @@ -160,7 +246,7 @@ router.patch('/api/updateRx/:id/metRequirements', async (req, res) => { /** * Route: 'doctorOrders/api/updateRx/:id/pickedUp' - * Description : 'Updates prescription dispense status based on mongo id to be picked up ' + * Description : 'Updates prescription dispense status to picked up and sends RxFill per NCPDP spec' */ router.patch('/api/updateRx/:id/pickedUp', async (req, res) => { let prescriberOrderNumber = null; @@ -178,40 +264,71 @@ router.patch('/api/updateRx/:id/pickedUp', async (req, res) => { return error; } + // Send RxFill per NCPDP spec to BOTH EHR and REMS Admin try { - // Reach out to EHR to update dispense status as XML const newRx = await NewRx.findOne({ prescriberOrderNumber: prescriberOrderNumber }); + + if (!newRx) { + console.log('NewRx not found for RxFill'); + return; + } + const rxFill = buildRxFill(newRx); - const status = await axios.post(env.EHR_RXFILL_URL, rxFill, { - headers: { - Accept: 'application/xml', // Expect that the Status that the EHR returns back is in XML - 'Content-Type': 'application/xml' // Tell the EHR that the RxFill is in XML + console.log('Sending RxFill per NCPDP workflow'); + + const config = getConfig(); + + if (config.useIntermediary) { + // Send to intermediary - it will forward to both EHR and REMS Admin + const endpoint = getRxFillEndpoint(); + console.log(`Sending RxFill to intermediary: ${endpoint}`); + await axios.post(endpoint, rxFill, { + headers: { 'Content-Type': 'application/xml' } + }); + } else { + // Send to EHR + try { + const ehrStatus = await axios.post(env.EHR_RXFILL_URL, rxFill, { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + }); + console.log('Sent RxFill to EHR, received status:', ehrStatus.data); + } catch (ehrError) { + console.log('Failed to send RxFill to EHR:', ehrError.message); } - }); - console.log('Sent RxFill to EHR and received status from EHR', status.data); - const remsAdminStatus = await axios.post(env.REMS_ADMIN_NCPDP, rxFill, { - headers: { - Accept: 'application/xml', // Expect that the Status that the rems admin returns back is in XML - 'Content-Type': 'application/xml' // Tell the rems admin that the RxFill is in XML + // Send to REMS Admin (required by NCPDP spec for REMS drugs) + const order = await doctorOrder.findOne({ prescriberOrderNumber }); + if (isRemsDrug(order)) { + try { + const remsAdminStatus = await axios.post( + env.REMS_ADMIN_NCPDP, + rxFill, + { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + } + ); + console.log('Sent RxFill to REMS Admin, received status:', remsAdminStatus.data); + } catch (remsError) { + console.log('Failed to send RxFill to REMS Admin:', remsError.message); + } } - }); - - console.log('Sent RxFill to rems admin and received status from rems admin: ', remsAdminStatus); + } } catch (error) { - console.log('Could not send RxFill to EHR', error); - return error; + console.log('Error in RxFill workflow:', error); } }); /** * Route : 'doctorOrders/api/getRx/patient/:patientName/drug/:simpleDrugName` * Description : 'Fetches first available doctor order based on patientFirstName, patientLastName and patientDOB' - * 'To retrieve a specific one for a drug on a given date, supply the drugNdcCode and rxDate in the query parameters' - * 'Required Parameters : patientFirstName, patientLastName patientDOB are part of the path' - * 'Optional Parameters : all remaining values in the orderSchema as query parameters (?drugNdcCode=0245-0571-01,rxDate=2020-07-11)' */ router.get('/api/getRx/:patientFirstName/:patientLastName/:patientDOB', async (req, res) => { var searchDict = { @@ -221,11 +338,8 @@ router.get('/api/getRx/:patientFirstName/:patientLastName/:patientDOB', async (r }; if (req.query && Object.keys(req.query).length > 0) { - // add the query parameters for (const prop in req.query) { - // verify that the parameter is in the orderSchema if (orderSchema.path(prop) != undefined) { - // add the parameters to the search query searchDict[prop] = req.query[prop]; } } @@ -249,6 +363,7 @@ router.delete('/api/deleteAll', async (req, res) => { }); const isRemsDrug = order => { + console.log(order); return medicationRequestToRemsAdmins.some(entry => { if (order.drugNdcCode && entry.ndc) { return order.drugNdcCode === entry.ndc; @@ -262,6 +377,11 @@ const isRemsDrug = order => { }); }; + +/** + * Get FHIR ETASU URL for the order + * Used for GuidanceResponse calls (View ETASU) + */ const getEtasuUrl = order => { let baseUrl; @@ -286,6 +406,10 @@ const getEtasuUrl = order => { return baseUrl ? etasuUrl : null; }; +/** + * Get FHIR GuidanceResponse for ETASU requirements + * Used by View ETASU button + */ const getGuidanceResponse = async order => { const etasuUrl = getEtasuUrl(order); @@ -368,31 +492,254 @@ const getGuidanceResponse = async order => { }; } - const response = await axios.post(etasuUrl, body, { - headers: { - 'content-type': 'application/json' + try { + const response = await axios.post(etasuUrl, body, { + headers: { + 'content-type': 'application/json' + } + }); + console.log('Retrieved FHIR GuidanceResponse', JSON.stringify(response.data, null, 4)); + console.log('URL', etasuUrl); + const responseResource = response.data.parameter?.[0]?.resource; + return responseResource; + } catch (error) { + console.log('Error fetching FHIR GuidanceResponse:', error.message); + return null; + } +}; + +/** + * Send NCPDP REMSInitiationRequest to REMS Admin + * Per NCPDP spec: Sent when prescription arrives to check REMS case status + */ +const sendREMSInitiationRequest = async order => { + try { + const newRx = await NewRx.findOne({ + prescriberOrderNumber: order.prescriberOrderNumber + }); + + if (!newRx) { + console.log('NewRx not found for REMSInitiationRequest'); + return null; } - }); - console.log('Retrieved order', JSON.stringify(response.data, null, 4)); - console.log('URL', etasuUrl); - const responseResource = response.data.parameter?.[0]?.resource; - return responseResource; + + const initiationRequest = buildREMSInitiationRequest(newRx); + console.log('Sending REMSInitiationRequest to REMS Admin'); + + console.log(initiationRequest) + + const endpoint = getNCPDPEndpoint(); + console.log(`Sending REMSInitiationRequest to: ${endpoint}`); + + const response = await axios.post( + endpoint, + initiationRequest, + { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + } + ); + + + const parsedResponse = await parseStringPromise(response.data, XML2JS_OPTS); + + console.log('Received REMSInitiationResponse'); + console.log('Response:', response.data); + + return parseREMSInitiationResponse(parsedResponse); + } catch (error) { + console.log('Error sending REMSInitiationRequest:', error.message); + return null; + } +}; + +/** + * Send NCPDP REMSRequest to REMS Admin for authorization + * Per NCPDP spec: Sent at pickup time for authorization check + */ +const sendREMSRequest = async order => { + try { + const newRx = await NewRx.findOne({ + prescriberOrderNumber: order.prescriberOrderNumber + }); + + if (!newRx) { + console.log('NewRx not found for REMSRequest'); + return null; + } + + if (!order.caseNumber) { + console.log('No case number - need REMSInitiationRequest first'); + return null; + } + + const remsRequest = buildREMSRequest(newRx, order.caseNumber); + console.log('Sending REMSRequest to REMS Admin for case:', order.caseNumber); + console.log(remsRequest) + + const endpoint = getNCPDPEndpoint(); + console.log(`Sending REMSRequest to: ${endpoint}`); + + const response = await axios.post( + endpoint, + remsRequest, + { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + } + ); + + const parsedResponse = await parseStringPromise(response.data, XML2JS_OPTS); + + console.log('Received REMSResponse'); + console.log('Response:', response.data); + return parseREMSResponse(parsedResponse); + } catch (error) { + console.log('Error sending REMSRequest:', error.message); + return null; + } }; -const getDispenseStatus = (order, guidanceResponse) => { - const isNotRemsDrug = !guidanceResponse; - const isRemsDrugAndMetEtasu = guidanceResponse?.status === 'success'; - const isPickedUp = order.dispenseStatus === 'Picked Up'; - if (isNotRemsDrug && order.dispenseStatus === 'Pending') return 'Approved'; - if (isRemsDrugAndMetEtasu) return 'Approved'; - if (isPickedUp) return 'Picked Up'; +/** + * Parse NCPDP REMSInitiationResponse per spec + * Extracts case info, status, and requirements + */ +const parseREMSInitiationResponse = parsedXml => { + const message = parsedXml?.message; + const body = message?.body; + const initResponse = body?.remsinitiationresponse; + console.log(message); + console.log(initResponse); + + if (!initResponse) { + console.log('No REMSInitiationResponse found'); + return null; + } + + const response = initResponse.response; + const responseStatus = response?.responsestatus; + + // Check for Closed status (requirements not met) + const closed = responseStatus?.closed; + if (closed) { + const reasonCode = closed.reasoncode; + const remsNote = closed.remsnote || ''; + + return { + status: 'CLOSED', + reasonCode: reasonCode, + remsNote: remsNote, + }; + } + + // Extract case ID and patient ID from successful initiation + const patient = initResponse.patient; + const humanPatient = patient?.humanpatient; + const identification = humanPatient?.identification; + const remsPatientId = identification?.remspatientid; + + // Check if there's a case number in the response + let caseNumber = null; + const medication = initResponse.medicationprescribed; + if (medication) { + // Some implementations include case number in initiation success + caseNumber = remsPatientId; // Often the case number is returned as patient ID + } + + return { + status: 'OPEN', + remsPatientId: remsPatientId, + caseNumber: caseNumber, + }; +}; + +/** + * Parse NCPDP REMSResponse per spec + * Extracts authorization status, case ID, and NCPDP rejection code + */ +const parseREMSResponse = parsedXml => { + const message = parsedXml?.message; + const body = message?.body; + const remsResponse = body?.remsresponse; + console.log(message); + console.log(remsResponse); + + if (!remsResponse) { + console.log('No REMSResponse found'); + return null; + } + + const request = remsResponse.request; + + const response = remsResponse.response; + const responseStatus = response?.responsestatus; + + // Check for APPROVED status + const approved = responseStatus?.approved; + if (approved) { + const caseId = approved.remscaseid; + const authNumber = approved.remsauthorizationnumber; + const authPeriod = approved.authorizationperiod; + const expiration = authPeriod?.expirationdate?.date; + + return { + status: 'APPROVED', + caseId: caseId, + authorizationNumber: authNumber, + authorizationExpiration: expiration, + remsNote: 'All REMS requirements have been met and verified. Authorization granted for dispensing.', + }; + } + + // Check for DENIED status + const denied = responseStatus?.denied; + if (denied) { + const caseId = denied.remscaseid; + const reasonCode = denied.deniedreasoncode; + const remsNote = denied.remsnote || ''; + + + + return { + status: 'DENIED', + caseId: caseId, + reasonCode: reasonCode, + remsNote: remsNote, + }; + } + + return null; +}; + +/** + * Determine dispense status based on NCPDP response + */ +const getDispenseStatus = (order, ncpdpResponse) => { + // Non-REMS drugs auto-approve + if (!ncpdpResponse) { + if (order.dispenseStatus === 'Pending') return 'Approved'; + if (order.dispenseStatus === 'Picked Up') return 'Picked Up'; + return order.dispenseStatus; + } + + // REMS drugs - check NCPDP response per spec + if (ncpdpResponse.status === 'APPROVED') { + return 'Approved'; + } + + if (order.dispenseStatus === 'Picked Up') { + return 'Picked Up'; + } + return 'Pending'; }; /** - * Description : 'Returns parsed NCPDP NewRx as JSON' - * In : NCPDP SCRIPT XML - * Return : Mongoose schema of a newOrder + * Parse NCPDP SCRIPT NewRx to order format */ async function parseNCPDPScript(newRx) { // Parsing XML NCPDP SCRIPT from EHR @@ -401,7 +748,7 @@ async function parseNCPDPScript(newRx) { const medicationPrescribed = newRx.Message.Body.NewRx.MedicationPrescribed; const incompleteOrder = { - orderId: newRx.Message.Header.MessageID.toString(), // Will need to return to this and use actual pt identifier or uuid + orderId: newRx.Message.Header.MessageID.toString(), caseNumber: newRx.Message.Header.AuthorizationNumber, prescriberOrderNumber: newRx.Message.Header.PrescriberOrderNumber, patientName: patient.HumanPatient.Name.FirstName + ' ' + patient.HumanPatient.Name.LastName, @@ -424,17 +771,15 @@ async function parseNCPDPScript(newRx) { simpleDrugName: medicationPrescribed.DrugDescription?.split(' ')[0], drugNdcCode: - medicationPrescribed.DrugCoded.ProductCode?.Code || - medicationPrescribed.DrugCoded.NDC || - null, + medicationPrescribed.DrugCoded.ProductCode?.Code || medicationPrescribed.DrugCoded.NDC || null, drugRxnormCode: medicationPrescribed.DrugCoded.DrugDBCode?.Code || null, rxDate: medicationPrescribed.WrittenDate.Date, - drugPrice: 200, // Add later? + drugPrice: 200, quantities: medicationPrescribed.Quantity.Value, total: 1800, - pickupDate: 'Tue Dec 13 2022', // Add later? + pickupDate: 'Tue Dec 13 2022', dispenseStatus: 'Pending' }; @@ -448,4 +793,25 @@ async function parseNCPDPScript(newRx) { return order; } -export default router; +/** + * Route: 'doctorOrders/api/config' + * Description: 'Get current pharmacy configuration' + */ +router.get('/api/config', async (_req, res) => { + const config = getConfig(); + console.log('Returning configuration:', config); + res.json(config); +}); + +/** + * Route: 'doctorOrders/api/config' + * Description: 'Update pharmacy configuration' + */ +router.post('/api/config', async (req, res) => { + const newConfig = updateConfig(req.body); + console.log('Configuration updated:', newConfig); + res.json(newConfig); +}); + + +export default router; \ No newline at end of file diff --git a/backend/src/routes/ncpdp.js b/backend/src/routes/ncpdp.js index 6bed68d..373d8f2 100644 --- a/backend/src/routes/ncpdp.js +++ b/backend/src/routes/ncpdp.js @@ -11,8 +11,8 @@ bpx(bodyParser); router.use( bodyParser.xml({ xmlParseOptions: { - normalize: true, // Trim whitespace inside text nodes - explicitArray: false // Only put nodes in array if >1 + normalize: true, + explicitArray: false } }) ); @@ -42,4 +42,4 @@ router.post('/script', async (req, res) => { console.log('Sent Status/Error'); }); -export default router; +export default router; \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 1154229..d238940 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Rems Intermediary UI + PIMS Pharmacy
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index afe7688..bc188bc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,9 @@ import { BrowserRouter as Router, Link, Route, Routes } from 'react-router-dom'; import './App.css'; import DoctorOrders from './views/DoctorOrders/DoctorOrders'; import Login from './views/Login/Login'; +import ProtectedRoute from './components/ProtectedRoute'; +import { AuthProvider } from './contexts/AuthContext'; +import ConfigToggle from './components/ConfigToggle'; import axios from 'axios'; axios.defaults.baseURL = process.env.REACT_APP_PIMS_BACKEND_URL @@ -17,37 +20,46 @@ const basename = process.env.REACT_APP_VITE_BASE?.replace(/\/$/, '') || ''; function App() { return ( - - -
- -
-
- -

Pharmacy

+ + + +
+ +
+
+ +

Pharmacy

+
+
+ + + + + + + +
-
- - - - - - -
-
- -
- - {/* Initial load to login page, will need to change to check for user authentication to load to correct page */} - } /> - }> - }> - - - + +
+ + } /> + } /> + + + + } + /> + + + + ); } -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/components/ConfigToggle.tsx b/frontend/src/components/ConfigToggle.tsx new file mode 100644 index 0000000..70598c4 --- /dev/null +++ b/frontend/src/components/ConfigToggle.tsx @@ -0,0 +1,85 @@ +import { IconButton, Menu, MenuItem, Switch, Typography, Box, Divider } from '@mui/material'; +import SettingsIcon from '@mui/icons-material/Settings'; +import { useState, useEffect } from 'react'; +import axios from 'axios'; + +export default function ConfigToggle() { + const [anchorEl, setAnchorEl] = useState(null); + const [useIntermediary, setUseIntermediary] = useState(false); + const open = Boolean(anchorEl); + + // Load config on mount + useEffect(() => { + const saved = localStorage.getItem('useIntermediary'); + if (saved !== null) { + setUseIntermediary(saved === 'true'); + } + }, []); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleToggle = async () => { + const newValue = !useIntermediary; + setUseIntermediary(newValue); + localStorage.setItem('useIntermediary', String(newValue)); + + // Update backend + try { + await axios.post('/doctorOrders/api/config', { useIntermediary: newValue }); + console.log('Configuration updated:', newValue ? 'Using Intermediary' : 'Direct Connection'); + } catch (error) { + console.error('Failed to update backend config:', error); + } + }; + + return ( + <> + + + + + + + + NCPDP Routing + + + + + + + + + Use Intermediary + + + {useIntermediary + ? 'Routing via intermediary' + : 'Direct to REMS Admin'} + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..6c467ed --- /dev/null +++ b/frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,16 @@ +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export default function ProtectedRoute({ children }: ProtectedRouteProps) { + const { isAuthenticated } = useAuth(); + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} diff --git a/frontend/src/config.json b/frontend/src/config.json index 6ce29d5..43a06ef 100644 --- a/frontend/src/config.json +++ b/frontend/src/config.json @@ -1,6 +1,6 @@ { "realm": "ClientFhirServer", "client": "pims-login", - "auth": "http://localhost:8180/auth", + "auth": "http://localhost:8180", "scopeId": "pims" } diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..cb06d2a --- /dev/null +++ b/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,36 @@ +import React, { createContext, useContext, useState } from 'react'; + +interface AuthContextType { + token: string | null; + login: (token: string) => void; + logout: () => void; + isAuthenticated: boolean; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [token, setToken] = useState(null); + + const login = (newToken: string) => { + setToken(newToken); + }; + + const logout = () => { + setToken(null); + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthProvider'); + } + return context; +} diff --git a/frontend/src/views/DoctorOrders/OrderCard/DenialNotification.tsx b/frontend/src/views/DoctorOrders/OrderCard/DenialNotification.tsx new file mode 100644 index 0000000..eb46178 --- /dev/null +++ b/frontend/src/views/DoctorOrders/OrderCard/DenialNotification.tsx @@ -0,0 +1,50 @@ +import { Alert, Snackbar } from '@mui/material'; +import React from 'react'; + +// NCPDP Denial Reason Code mapping +const DENIAL_CODE_MESSAGES: Record = { + EM: 'Patient Enrollment/Certification Required', + ES: 'Prescriber Enrollment/Certification Required', + EO: 'Pharmacy Enrollment/Certification Required', + EC: 'Case Information Missing or Invalid', + ER: 'REMS Program Error', + EX: 'Prescriber Deactivated/Decertified', + EY: 'Pharmacy Deactivated/Decertified', + EZ: 'Patient Deactivated/Decertified' +}; + +type DenialNotificationProps = { + open: boolean; + onClose: () => void; + denialCode?: string; + remsNote?: string; +} +const DenialNotification = (props: DenialNotificationProps) => { + const getMessage = () => { + if (props.remsNote) { + return props.remsNote; + } + + // Fallback to hardcoded messages if remsNote is empty + if (props.denialCode) { + return DENIAL_CODE_MESSAGES[props.denialCode] || `Denial Code: ${props.denialCode}`; + } + + return 'Order verification denied'; + }; + + return ( + + + {getMessage()} + + + ); +}; + +export default DenialNotification; \ No newline at end of file diff --git a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.test.tsx b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.test.tsx index 47bbd62..4f0d81c 100644 --- a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.test.tsx +++ b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.test.tsx @@ -14,14 +14,14 @@ const doctorOrders = [ drugNdcCode: '65597-407-20', drugPrice: 200, metRequirements: [], - patientCity: 'Winterfell', + patientCity: 'Boston', patientCountry: 'US', patientDOB: '1996-06-01', - patientFirstName: 'Jon', + patientFirstName: 'John', patientLastName: 'Snow', - patientName: 'Jon Snow', - patientPostalCode: '00008', - patientStateProvince: 'Westeros', + patientName: 'John Snow', + patientPostalCode: '02134', + patientStateProvince: 'MA', pickupDate: 'Tue Dec 13 2022', quantities: '90', rxDate: '2020-07-11', @@ -47,7 +47,7 @@ describe('', () => { render(); await waitFor(() => { - expect(screen.getByText(/Jon Snow/i)).toBeInTheDocument(); + expect(screen.getByText(/John Snow/i)).toBeInTheDocument(); expect(screen.getByText(/1996/i)).toBeInTheDocument(); expect(screen.getByText(/Turalio/i)).toBeInTheDocument(); expect(screen.getByText(/Pending/i)).toBeInTheDocument(); diff --git a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx index 998678b..84e099b 100644 --- a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx +++ b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx @@ -34,6 +34,8 @@ export type DoctorOrder = { total?: number; pickupDate?: string; dispenseStatus?: string; + denialReasonCode?: string; + remsNote?: string; metRequirements: | { name: string; @@ -194,4 +196,4 @@ const OrderCard = (props: { tabStatus: TabStatus }) => { } }; -export default OrderCard; +export default OrderCard; \ No newline at end of file diff --git a/frontend/src/views/DoctorOrders/OrderCard/VerifyButton.tsx b/frontend/src/views/DoctorOrders/OrderCard/VerifyButton.tsx index 0b6f7c4..74cbfe0 100644 --- a/frontend/src/views/DoctorOrders/OrderCard/VerifyButton.tsx +++ b/frontend/src/views/DoctorOrders/OrderCard/VerifyButton.tsx @@ -1,26 +1,55 @@ import Button from '@mui/material/Button'; import axios from 'axios'; +import { useState } from 'react'; import { DoctorOrder } from './OrderCard'; +import DenialNotification from './DenialNotification'; type VerifyButtonProps = { row: DoctorOrder; getAllDoctorOrders: () => Promise }; const VerifyButton = (props: VerifyButtonProps) => { + const [showDenial, setShowDenial] = useState(false); + const [denialCode, setDenialCode] = useState(); + const [remsNote, setRemsNote] = useState(); + const verifyOrder = () => { const url = '/doctorOrders/api/updateRx/' + props.row._id; axios .patch(url) .then(function (response) { + const updatedOrder = response.data; + + // Check if the order was denied by NCPDP REMS + if (updatedOrder.denialReasonCode) { + setDenialCode(updatedOrder.denialReasonCode); + setRemsNote(updatedOrder.remsNote); + setShowDenial(true); + } + props.getAllDoctorOrders(); console.log(response.data); }) - .catch(error => console.error('Error', error)); + .catch(error => { + console.error('Error', error); + }); + }; + + const handleCloseDenial = () => { + setShowDenial(false); }; return ( - + <> + + + ); }; -export default VerifyButton; +export default VerifyButton; \ No newline at end of file diff --git a/frontend/src/views/Login/Login.tsx b/frontend/src/views/Login/Login.tsx index e2335b3..8f024cc 100644 --- a/frontend/src/views/Login/Login.tsx +++ b/frontend/src/views/Login/Login.tsx @@ -3,10 +3,14 @@ import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import { Avatar, Box, Button, Container, CssBaseline, TextField, Typography } from '@mui/material'; import axios from 'axios'; import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../contexts/AuthContext'; import config from '../../config.json'; export default function Login() { - const [token, setToken] = React.useState(null); + const { login, isAuthenticated } = useAuth(); + const navigate = useNavigate(); + const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); const data = new FormData(event.currentTarget); @@ -23,16 +27,16 @@ export default function Login() { withCredentials: true }) .then(result => { - // do something with the token const scope = result.data.scope.split(' ').includes(config.scopeId); if (scope) { - setToken(result.data.access_token); + login(result.data.access_token); + navigate('/DoctorOrders'); } else { console.error('Unauthorized User'); } }) .catch(err => { - if (err.response.status === 401) { + if (err.response?.status === 401) { console.error('Unknown user'); } else { console.error(err); @@ -52,7 +56,7 @@ export default function Login() { alignItems: 'center' }} > - {token ? ( + {isAuthenticated ? ( diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 187cc8c..86beb21 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,11 +1,9 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; - import dotenv from 'dotenv'; dotenv.config({ path: '.env' }); // load env vars from .env export default defineConfig({ - // depending on your application, base can also be "/" base: process.env.REACT_APP_VITE_BASE || '', plugins: [react()], preview: {