From 643a791cc9a49d92309cfd3c68bd6992832a7045 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 14 Jun 2025 12:23:59 +0000 Subject: [PATCH] treatment --- .gitignore | 5 + app-shell/src/_schema.json | 7 +- backend/src/db/api/clinics.js | 8 + backend/src/db/api/diagnoses.js | 357 +++++++++++++ backend/src/db/api/patients.js | 4 + backend/src/db/api/prescriptions.js | 387 ++++++++++++++ backend/src/db/api/treatments.js | 88 ++++ backend/src/db/api/users.js | 4 + backend/src/db/migrations/1749903339789.js | 54 ++ backend/src/db/migrations/1749903373743.js | 54 ++ backend/src/db/migrations/1749903399272.js | 90 ++++ backend/src/db/migrations/1749903433079.js | 54 ++ backend/src/db/migrations/1749903455398.js | 90 ++++ backend/src/db/migrations/1749903507212.js | 47 ++ backend/src/db/migrations/1749903531991.js | 47 ++ backend/src/db/migrations/1749903555044.js | 49 ++ backend/src/db/migrations/1749903605083.js | 49 ++ backend/src/db/models/clinics.js | 16 + backend/src/db/models/diagnoses.js | 69 +++ backend/src/db/models/patients.js | 8 + backend/src/db/models/prescriptions.js | 73 +++ backend/src/db/models/treatments.js | 24 + backend/src/db/models/users.js | 8 + .../db/seeders/20200430130760-user-roles.js | 102 ++++ .../db/seeders/20231127130745-sample-data.js | 422 ++++++++++++++- backend/src/db/seeders/20250614121639.js | 87 ++++ backend/src/db/seeders/20250614121735.js | 87 ++++ backend/src/index.js | 16 + backend/src/routes/diagnoses.js | 455 ++++++++++++++++ backend/src/routes/prescriptions.js | 462 +++++++++++++++++ backend/src/services/diagnoses.js | 114 ++++ backend/src/services/prescriptions.js | 117 +++++ backend/src/services/search.js | 4 + frontend/json/runtimeError.json | 1 + .../components/Diagnoses/CardDiagnoses.tsx | 125 +++++ .../components/Diagnoses/ListDiagnoses.tsx | 103 ++++ .../components/Diagnoses/TableDiagnoses.tsx | 487 ++++++++++++++++++ .../Diagnoses/configureDiagnosesCols.tsx | 106 ++++ .../Prescriptions/CardPrescriptions.tsx | 147 ++++++ .../Prescriptions/ListPrescriptions.tsx | 111 ++++ .../Prescriptions/TablePrescriptions.tsx | 487 ++++++++++++++++++ .../configurePrescriptionsCols.tsx | 118 +++++ .../components/Treatments/CardTreatments.tsx | 22 + .../components/Treatments/ListTreatments.tsx | 16 + .../Treatments/configureTreatmentsCols.tsx | 40 ++ .../components/WebPageComponents/Footer.tsx | 2 +- .../components/WebPageComponents/Header.tsx | 4 +- frontend/src/helpers/dataFormatter.js | 19 + frontend/src/menuAside.ts | 16 + frontend/src/pages/clinics/clinics-view.tsx | 86 ++++ frontend/src/pages/dashboard.tsx | 70 +++ .../src/pages/diagnoses/[diagnosesId].tsx | 162 ++++++ .../src/pages/diagnoses/diagnoses-edit.tsx | 160 ++++++ .../src/pages/diagnoses/diagnoses-list.tsx | 167 ++++++ .../src/pages/diagnoses/diagnoses-new.tsx | 128 +++++ .../src/pages/diagnoses/diagnoses-table.tsx | 166 ++++++ .../src/pages/diagnoses/diagnoses-view.tsx | 104 ++++ frontend/src/pages/patients/patients-view.tsx | 37 ++ .../pages/prescriptions/[prescriptionsId].tsx | 170 ++++++ .../prescriptions/prescriptions-edit.tsx | 168 ++++++ .../prescriptions/prescriptions-list.tsx | 171 ++++++ .../pages/prescriptions/prescriptions-new.tsx | 136 +++++ .../prescriptions/prescriptions-table.tsx | 170 ++++++ .../prescriptions/prescriptions-view.tsx | 109 ++++ .../src/pages/treatments/[treatmentsId].tsx | 26 + .../src/pages/treatments/treatments-edit.tsx | 26 + .../src/pages/treatments/treatments-list.tsx | 4 + .../src/pages/treatments/treatments-new.tsx | 24 + .../src/pages/treatments/treatments-table.tsx | 4 + .../src/pages/treatments/treatments-view.tsx | 98 ++++ frontend/src/pages/users/users-view.tsx | 37 ++ frontend/src/pages/web_pages/home.tsx | 2 +- frontend/src/pages/web_pages/services.tsx | 2 +- .../src/stores/diagnoses/diagnosesSlice.ts | 236 +++++++++ .../prescriptions/prescriptionsSlice.ts | 250 +++++++++ frontend/src/stores/store.ts | 4 + 76 files changed, 7963 insertions(+), 16 deletions(-) create mode 100644 backend/src/db/api/diagnoses.js create mode 100644 backend/src/db/api/prescriptions.js create mode 100644 backend/src/db/migrations/1749903339789.js create mode 100644 backend/src/db/migrations/1749903373743.js create mode 100644 backend/src/db/migrations/1749903399272.js create mode 100644 backend/src/db/migrations/1749903433079.js create mode 100644 backend/src/db/migrations/1749903455398.js create mode 100644 backend/src/db/migrations/1749903507212.js create mode 100644 backend/src/db/migrations/1749903531991.js create mode 100644 backend/src/db/migrations/1749903555044.js create mode 100644 backend/src/db/migrations/1749903605083.js create mode 100644 backend/src/db/models/diagnoses.js create mode 100644 backend/src/db/models/prescriptions.js create mode 100644 backend/src/db/seeders/20250614121639.js create mode 100644 backend/src/db/seeders/20250614121735.js create mode 100644 backend/src/routes/diagnoses.js create mode 100644 backend/src/routes/prescriptions.js create mode 100644 backend/src/services/diagnoses.js create mode 100644 backend/src/services/prescriptions.js create mode 100644 frontend/json/runtimeError.json create mode 100644 frontend/src/components/Diagnoses/CardDiagnoses.tsx create mode 100644 frontend/src/components/Diagnoses/ListDiagnoses.tsx create mode 100644 frontend/src/components/Diagnoses/TableDiagnoses.tsx create mode 100644 frontend/src/components/Diagnoses/configureDiagnosesCols.tsx create mode 100644 frontend/src/components/Prescriptions/CardPrescriptions.tsx create mode 100644 frontend/src/components/Prescriptions/ListPrescriptions.tsx create mode 100644 frontend/src/components/Prescriptions/TablePrescriptions.tsx create mode 100644 frontend/src/components/Prescriptions/configurePrescriptionsCols.tsx create mode 100644 frontend/src/pages/diagnoses/[diagnosesId].tsx create mode 100644 frontend/src/pages/diagnoses/diagnoses-edit.tsx create mode 100644 frontend/src/pages/diagnoses/diagnoses-list.tsx create mode 100644 frontend/src/pages/diagnoses/diagnoses-new.tsx create mode 100644 frontend/src/pages/diagnoses/diagnoses-table.tsx create mode 100644 frontend/src/pages/diagnoses/diagnoses-view.tsx create mode 100644 frontend/src/pages/prescriptions/[prescriptionsId].tsx create mode 100644 frontend/src/pages/prescriptions/prescriptions-edit.tsx create mode 100644 frontend/src/pages/prescriptions/prescriptions-list.tsx create mode 100644 frontend/src/pages/prescriptions/prescriptions-new.tsx create mode 100644 frontend/src/pages/prescriptions/prescriptions-table.tsx create mode 100644 frontend/src/pages/prescriptions/prescriptions-view.tsx create mode 100644 frontend/src/stores/diagnoses/diagnosesSlice.ts create mode 100644 frontend/src/stores/prescriptions/prescriptionsSlice.ts diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 889db54..beef526 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"9yKvp+NWjA3WV/t7\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"9yKvp+NWjA3WV/t7\",\"encryptedData\":\"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\"}", + "treatment": "{\"iv\":\"X4se3zAgSUeXhGXX\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/clinics.js b/backend/src/db/api/clinics.js index c122287..6eda90b 100644 --- a/backend/src/db/api/clinics.js +++ b/backend/src/db/api/clinics.js @@ -149,6 +149,14 @@ module.exports = class ClinicsDBApi { transaction, }); + output.diagnoses_clinics = await clinics.getDiagnoses_clinics({ + transaction, + }); + + output.prescriptions_clinics = await clinics.getPrescriptions_clinics({ + transaction, + }); + return output; } diff --git a/backend/src/db/api/diagnoses.js b/backend/src/db/api/diagnoses.js new file mode 100644 index 0000000..487b373 --- /dev/null +++ b/backend/src/db/api/diagnoses.js @@ -0,0 +1,357 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class DiagnosesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const diagnoses = await db.diagnoses.create( + { + id: data.id || undefined, + + code: data.code || null, + notes: data.notes || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await diagnoses.setClinics(data.clinics || null, { + transaction, + }); + + await diagnoses.setTreatment_id(data.treatment_id || null, { + transaction, + }); + + return diagnoses; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const diagnosesData = data.map((item, index) => ({ + id: item.id || undefined, + + code: item.code || null, + notes: item.notes || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const diagnoses = await db.diagnoses.bulkCreate(diagnosesData, { + transaction, + }); + + // For each item created, replace relation files + + return diagnoses; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const diagnoses = await db.diagnoses.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.code !== undefined) updatePayload.code = data.code; + + if (data.notes !== undefined) updatePayload.notes = data.notes; + + updatePayload.updatedById = currentUser.id; + + await diagnoses.update(updatePayload, { transaction }); + + if (data.clinics !== undefined) { + await diagnoses.setClinics( + data.clinics, + + { transaction }, + ); + } + + if (data.treatment_id !== undefined) { + await diagnoses.setTreatment_id( + data.treatment_id, + + { transaction }, + ); + } + + return diagnoses; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const diagnoses = await db.diagnoses.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of diagnoses) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of diagnoses) { + await record.destroy({ transaction }); + } + }); + + return diagnoses; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const diagnoses = await db.diagnoses.findByPk(id, options); + + await diagnoses.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await diagnoses.destroy({ + transaction, + }); + + return diagnoses; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const diagnoses = await db.diagnoses.findOne({ where }, { transaction }); + + if (!diagnoses) { + return diagnoses; + } + + const output = diagnoses.get({ plain: true }); + + output.clinics = await diagnoses.getClinics({ + transaction, + }); + + output.treatment_id = await diagnoses.getTreatment_id({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userClinics = (user && user.clinics?.id) || null; + + if (userClinics) { + if (options?.currentUser?.clinicsId) { + where.clinicsId = options.currentUser.clinicsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.clinics, + as: 'clinics', + }, + + { + model: db.treatments, + as: 'treatment_id', + + where: filter.treatment_id + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.treatment_id + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + description: { + [Op.or]: filter.treatment_id + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.code) { + where = { + ...where, + [Op.and]: Utils.ilike('diagnoses', 'code', filter.code), + }; + } + + if (filter.notes) { + where = { + ...where, + [Op.and]: Utils.ilike('diagnoses', 'notes', filter.notes), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.clinics) { + const listItems = filter.clinics.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + clinicsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.clinicsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.diagnoses.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('diagnoses', 'code', query), + ], + }; + } + + const records = await db.diagnoses.findAll({ + attributes: ['id', 'code'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['code', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.code, + })); + } +}; diff --git a/backend/src/db/api/patients.js b/backend/src/db/api/patients.js index 27485cc..27973e3 100644 --- a/backend/src/db/api/patients.js +++ b/backend/src/db/api/patients.js @@ -168,6 +168,10 @@ module.exports = class PatientsDBApi { transaction, }); + output.treatments_patient_id = await patients.getTreatments_patient_id({ + transaction, + }); + output.clinic = await patients.getClinic({ transaction, }); diff --git a/backend/src/db/api/prescriptions.js b/backend/src/db/api/prescriptions.js new file mode 100644 index 0000000..8c31f65 --- /dev/null +++ b/backend/src/db/api/prescriptions.js @@ -0,0 +1,387 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class PrescriptionsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const prescriptions = await db.prescriptions.create( + { + id: data.id || undefined, + + medication: data.medication || null, + dosage: data.dosage || null, + instructions: data.instructions || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await prescriptions.setClinics(data.clinics || null, { + transaction, + }); + + await prescriptions.setTreatment_id(data.treatment_id || null, { + transaction, + }); + + return prescriptions; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const prescriptionsData = data.map((item, index) => ({ + id: item.id || undefined, + + medication: item.medication || null, + dosage: item.dosage || null, + instructions: item.instructions || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const prescriptions = await db.prescriptions.bulkCreate(prescriptionsData, { + transaction, + }); + + // For each item created, replace relation files + + return prescriptions; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const prescriptions = await db.prescriptions.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.medication !== undefined) + updatePayload.medication = data.medication; + + if (data.dosage !== undefined) updatePayload.dosage = data.dosage; + + if (data.instructions !== undefined) + updatePayload.instructions = data.instructions; + + updatePayload.updatedById = currentUser.id; + + await prescriptions.update(updatePayload, { transaction }); + + if (data.clinics !== undefined) { + await prescriptions.setClinics( + data.clinics, + + { transaction }, + ); + } + + if (data.treatment_id !== undefined) { + await prescriptions.setTreatment_id( + data.treatment_id, + + { transaction }, + ); + } + + return prescriptions; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const prescriptions = await db.prescriptions.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of prescriptions) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of prescriptions) { + await record.destroy({ transaction }); + } + }); + + return prescriptions; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const prescriptions = await db.prescriptions.findByPk(id, options); + + await prescriptions.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await prescriptions.destroy({ + transaction, + }); + + return prescriptions; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const prescriptions = await db.prescriptions.findOne( + { where }, + { transaction }, + ); + + if (!prescriptions) { + return prescriptions; + } + + const output = prescriptions.get({ plain: true }); + + output.clinics = await prescriptions.getClinics({ + transaction, + }); + + output.treatment_id = await prescriptions.getTreatment_id({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userClinics = (user && user.clinics?.id) || null; + + if (userClinics) { + if (options?.currentUser?.clinicsId) { + where.clinicsId = options.currentUser.clinicsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.clinics, + as: 'clinics', + }, + + { + model: db.treatments, + as: 'treatment_id', + + where: filter.treatment_id + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.treatment_id + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + description: { + [Op.or]: filter.treatment_id + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.medication) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'prescriptions', + 'medication', + filter.medication, + ), + }; + } + + if (filter.dosage) { + where = { + ...where, + [Op.and]: Utils.ilike('prescriptions', 'dosage', filter.dosage), + }; + } + + if (filter.instructions) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'prescriptions', + 'instructions', + filter.instructions, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.clinics) { + const listItems = filter.clinics.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + clinicsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.clinicsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.prescriptions.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('prescriptions', 'medication', query), + ], + }; + } + + const records = await db.prescriptions.findAll({ + attributes: ['id', 'medication'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['medication', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.medication, + })); + } +}; diff --git a/backend/src/db/api/treatments.js b/backend/src/db/api/treatments.js index 3ff296e..6e5671e 100644 --- a/backend/src/db/api/treatments.js +++ b/backend/src/db/api/treatments.js @@ -32,6 +32,14 @@ module.exports = class TreatmentsDBApi { transaction, }); + await treatments.setPatient_id(data.patient_id || null, { + transaction, + }); + + await treatments.setDoctor_id(data.doctor_id || null, { + transaction, + }); + return treatments; } @@ -95,6 +103,22 @@ module.exports = class TreatmentsDBApi { ); } + if (data.patient_id !== undefined) { + await treatments.setPatient_id( + data.patient_id, + + { transaction }, + ); + } + + if (data.doctor_id !== undefined) { + await treatments.setDoctor_id( + data.doctor_id, + + { transaction }, + ); + } + return treatments; } @@ -156,6 +180,10 @@ module.exports = class TreatmentsDBApi { const output = treatments.get({ plain: true }); + output.diagnoses_treatment_id = await treatments.getDiagnoses_treatment_id({ + transaction, + }); + output.appointment = await treatments.getAppointment({ transaction, }); @@ -164,6 +192,14 @@ module.exports = class TreatmentsDBApi { transaction, }); + output.patient_id = await treatments.getPatient_id({ + transaction, + }); + + output.doctor_id = await treatments.getDoctor_id({ + transaction, + }); + return output; } @@ -219,6 +255,58 @@ module.exports = class TreatmentsDBApi { model: db.clinics, as: 'clinics', }, + + { + model: db.patients, + as: 'patient_id', + + where: filter.patient_id + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.patient_id + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + first_name: { + [Op.or]: filter.patient_id + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.users, + as: 'doctor_id', + + where: filter.doctor_id + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.doctor_id + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.doctor_id + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, ]; if (filter) { diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 94cad69..2b02b5b 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -283,6 +283,10 @@ module.exports = class UsersDBApi { transaction, }); + output.treatments_doctor_id = await users.getTreatments_doctor_id({ + transaction, + }); + output.avatar = await users.getAvatar({ transaction, }); diff --git a/backend/src/db/migrations/1749903339789.js b/backend/src/db/migrations/1749903339789.js new file mode 100644 index 0000000..3e507a6 --- /dev/null +++ b/backend/src/db/migrations/1749903339789.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'treatments', + 'patient_idId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'patients', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('treatments', 'patient_idId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1749903373743.js b/backend/src/db/migrations/1749903373743.js new file mode 100644 index 0000000..aab8445 --- /dev/null +++ b/backend/src/db/migrations/1749903373743.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'treatments', + 'doctor_idId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('treatments', 'doctor_idId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1749903399272.js b/backend/src/db/migrations/1749903399272.js new file mode 100644 index 0000000..fce3c38 --- /dev/null +++ b/backend/src/db/migrations/1749903399272.js @@ -0,0 +1,90 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'diagnoses', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'diagnoses', + 'clinicsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'clinics', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('diagnoses', 'clinicsId', { + transaction, + }); + + await queryInterface.dropTable('diagnoses', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1749903433079.js b/backend/src/db/migrations/1749903433079.js new file mode 100644 index 0000000..22ef319 --- /dev/null +++ b/backend/src/db/migrations/1749903433079.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'diagnoses', + 'treatment_idId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'treatments', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('diagnoses', 'treatment_idId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1749903455398.js b/backend/src/db/migrations/1749903455398.js new file mode 100644 index 0000000..ca951a2 --- /dev/null +++ b/backend/src/db/migrations/1749903455398.js @@ -0,0 +1,90 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'prescriptions', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'prescriptions', + 'clinicsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'clinics', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('prescriptions', 'clinicsId', { + transaction, + }); + + await queryInterface.dropTable('prescriptions', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1749903507212.js b/backend/src/db/migrations/1749903507212.js new file mode 100644 index 0000000..37e86fc --- /dev/null +++ b/backend/src/db/migrations/1749903507212.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'diagnoses', + 'code', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('diagnoses', 'code', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1749903531991.js b/backend/src/db/migrations/1749903531991.js new file mode 100644 index 0000000..a810a97 --- /dev/null +++ b/backend/src/db/migrations/1749903531991.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'diagnoses', + 'notes', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('diagnoses', 'notes', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1749903555044.js b/backend/src/db/migrations/1749903555044.js new file mode 100644 index 0000000..a49eabf --- /dev/null +++ b/backend/src/db/migrations/1749903555044.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'prescriptions', + 'medication', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('prescriptions', 'medication', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1749903605083.js b/backend/src/db/migrations/1749903605083.js new file mode 100644 index 0000000..ab78b38 --- /dev/null +++ b/backend/src/db/migrations/1749903605083.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'prescriptions', + 'instructions', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('prescriptions', 'instructions', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/clinics.js b/backend/src/db/models/clinics.js index 48ab139..2ac87ab 100644 --- a/backend/src/db/models/clinics.js +++ b/backend/src/db/models/clinics.js @@ -82,6 +82,22 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.clinics.hasMany(db.diagnoses, { + as: 'diagnoses_clinics', + foreignKey: { + name: 'clinicsId', + }, + constraints: false, + }); + + db.clinics.hasMany(db.prescriptions, { + as: 'prescriptions_clinics', + foreignKey: { + name: 'clinicsId', + }, + constraints: false, + }); + //end loop db.clinics.belongsTo(db.users, { diff --git a/backend/src/db/models/diagnoses.js b/backend/src/db/models/diagnoses.js new file mode 100644 index 0000000..6c607a0 --- /dev/null +++ b/backend/src/db/models/diagnoses.js @@ -0,0 +1,69 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const diagnoses = sequelize.define( + 'diagnoses', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + code: { + type: DataTypes.TEXT, + }, + + notes: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + diagnoses.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.diagnoses.belongsTo(db.clinics, { + as: 'clinics', + foreignKey: { + name: 'clinicsId', + }, + constraints: false, + }); + + db.diagnoses.belongsTo(db.treatments, { + as: 'treatment_id', + foreignKey: { + name: 'treatment_idId', + }, + constraints: false, + }); + + db.diagnoses.belongsTo(db.users, { + as: 'createdBy', + }); + + db.diagnoses.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return diagnoses; +}; diff --git a/backend/src/db/models/patients.js b/backend/src/db/models/patients.js index ffd2dff..bb3cd20 100644 --- a/backend/src/db/models/patients.js +++ b/backend/src/db/models/patients.js @@ -54,6 +54,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.patients.hasMany(db.treatments, { + as: 'treatments_patient_id', + foreignKey: { + name: 'patient_idId', + }, + constraints: false, + }); + //end loop db.patients.belongsTo(db.clinics, { diff --git a/backend/src/db/models/prescriptions.js b/backend/src/db/models/prescriptions.js new file mode 100644 index 0000000..bf05343 --- /dev/null +++ b/backend/src/db/models/prescriptions.js @@ -0,0 +1,73 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const prescriptions = sequelize.define( + 'prescriptions', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + medication: { + type: DataTypes.TEXT, + }, + + dosage: { + type: DataTypes.TEXT, + }, + + instructions: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + prescriptions.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.prescriptions.belongsTo(db.clinics, { + as: 'clinics', + foreignKey: { + name: 'clinicsId', + }, + constraints: false, + }); + + db.prescriptions.belongsTo(db.treatments, { + as: 'treatment_id', + foreignKey: { + name: 'treatment_idId', + }, + constraints: false, + }); + + db.prescriptions.belongsTo(db.users, { + as: 'createdBy', + }); + + db.prescriptions.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return prescriptions; +}; diff --git a/backend/src/db/models/treatments.js b/backend/src/db/models/treatments.js index fd0fee1..60276bf 100644 --- a/backend/src/db/models/treatments.js +++ b/backend/src/db/models/treatments.js @@ -38,6 +38,14 @@ module.exports = function (sequelize, DataTypes) { treatments.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.treatments.hasMany(db.diagnoses, { + as: 'diagnoses_treatment_id', + foreignKey: { + name: 'treatment_idId', + }, + constraints: false, + }); + //end loop db.treatments.belongsTo(db.appointments, { @@ -56,6 +64,22 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.treatments.belongsTo(db.patients, { + as: 'patient_id', + foreignKey: { + name: 'patient_idId', + }, + constraints: false, + }); + + db.treatments.belongsTo(db.users, { + as: 'doctor_id', + foreignKey: { + name: 'doctor_idId', + }, + constraints: false, + }); + db.treatments.belongsTo(db.users, { as: 'createdBy', }); diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 68c4d54..3572843 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -110,6 +110,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.users.hasMany(db.treatments, { + as: 'treatments_doctor_id', + foreignKey: { + name: 'doctor_idId', + }, + constraints: false, + }); + //end loop db.users.belongsTo(db.roles, { diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 29cef26..13929d4 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -114,6 +114,8 @@ module.exports = { 'roles', 'permissions', 'clinics', + 'diagnoses', + 'prescriptions', , ]; await queryInterface.bulkInsert( @@ -664,6 +666,56 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_TREATMENTS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_DIAGNOSES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_DIAGNOSES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_DIAGNOSES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_DIAGNOSES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PRESCRIPTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PRESCRIPTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PRESCRIPTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PRESCRIPTIONS'), + }, + { createdAt, updatedAt, @@ -864,6 +916,56 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_CLINICS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_DIAGNOSES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_DIAGNOSES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_DIAGNOSES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_DIAGNOSES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_PRESCRIPTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_PRESCRIPTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_PRESCRIPTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_PRESCRIPTIONS'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 55befb7..dc749f4 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -11,6 +11,10 @@ const Treatments = db.treatments; const Clinics = db.clinics; +const Diagnoses = db.diagnoses; + +const Prescriptions = db.prescriptions; + const AppointmentsData = [ { // type code here for "relation_one" field @@ -21,7 +25,7 @@ const AppointmentsData = [ end_time: new Date('2023-11-01T09:30:00Z'), - status: 'scheduled', + status: 'completed', // type code here for "relation_one" field }, @@ -35,7 +39,7 @@ const AppointmentsData = [ end_time: new Date('2023-11-01T10:30:00Z'), - status: 'scheduled', + status: 'completed', // type code here for "relation_one" field }, @@ -63,7 +67,7 @@ const AppointmentsData = [ end_time: new Date('2023-11-01T13:30:00Z'), - status: 'completed', + status: 'cancelled', // type code here for "relation_one" field }, @@ -133,7 +137,7 @@ const PaymentsData = [ amount: 150, - method: 'creditcard', + method: 'insurance', payment_date: new Date('2023-11-01T09:30:00Z'), @@ -145,7 +149,7 @@ const PaymentsData = [ amount: 100, - method: 'cash', + method: 'insurance', payment_date: new Date('2023-11-01T10:30:00Z'), @@ -157,7 +161,7 @@ const PaymentsData = [ amount: 200, - method: 'insurance', + method: 'creditcard', payment_date: new Date('2023-11-01T11:30:00Z'), @@ -169,7 +173,7 @@ const PaymentsData = [ amount: 75, - method: 'insurance', + method: 'creditcard', payment_date: new Date('2023-11-01T13:30:00Z'), @@ -186,6 +190,10 @@ const TreatmentsData = [ cost: 150, // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -196,6 +204,10 @@ const TreatmentsData = [ cost: 100, // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -206,6 +218,10 @@ const TreatmentsData = [ cost: 200, // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -216,6 +232,10 @@ const TreatmentsData = [ cost: 75, // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field }, ]; @@ -237,6 +257,98 @@ const ClinicsData = [ }, ]; +const DiagnosesData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + code: 'Rudolf Virchow', + + notes: 'William Bayliss', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + code: 'Claude Bernard', + + notes: 'Albert Einstein', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + code: 'Gregor Mendel', + + notes: 'Marie Curie', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + code: 'Lynn Margulis', + + notes: 'Sheldon Glashow', + }, +]; + +const PrescriptionsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + medication: 'Marie Curie', + + dosage: 'Murray Gell-Mann', + + instructions: 'Marcello Malpighi', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + medication: 'William Herschel', + + dosage: 'Hans Bethe', + + instructions: 'Paul Dirac', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + medication: 'Werner Heisenberg', + + dosage: 'Emil Kraepelin', + + instructions: 'Marie Curie', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + medication: 'William Bayliss', + + dosage: 'Ernst Mayr', + + instructions: 'Anton van Leeuwenhoek', + }, +]; + // Similar logic for "relation_many" async function associateUserWithClinic() { @@ -699,6 +811,282 @@ async function associateTreatmentWithClinic() { } } +async function associateTreatmentWithPatient_id() { + const relatedPatient_id0 = await Patients.findOne({ + offset: Math.floor(Math.random() * (await Patients.count())), + }); + const Treatment0 = await Treatments.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Treatment0?.setPatient_id) { + await Treatment0.setPatient_id(relatedPatient_id0); + } + + const relatedPatient_id1 = await Patients.findOne({ + offset: Math.floor(Math.random() * (await Patients.count())), + }); + const Treatment1 = await Treatments.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Treatment1?.setPatient_id) { + await Treatment1.setPatient_id(relatedPatient_id1); + } + + const relatedPatient_id2 = await Patients.findOne({ + offset: Math.floor(Math.random() * (await Patients.count())), + }); + const Treatment2 = await Treatments.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Treatment2?.setPatient_id) { + await Treatment2.setPatient_id(relatedPatient_id2); + } + + const relatedPatient_id3 = await Patients.findOne({ + offset: Math.floor(Math.random() * (await Patients.count())), + }); + const Treatment3 = await Treatments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Treatment3?.setPatient_id) { + await Treatment3.setPatient_id(relatedPatient_id3); + } +} + +async function associateTreatmentWithDoctor_id() { + const relatedDoctor_id0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Treatment0 = await Treatments.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Treatment0?.setDoctor_id) { + await Treatment0.setDoctor_id(relatedDoctor_id0); + } + + const relatedDoctor_id1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Treatment1 = await Treatments.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Treatment1?.setDoctor_id) { + await Treatment1.setDoctor_id(relatedDoctor_id1); + } + + const relatedDoctor_id2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Treatment2 = await Treatments.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Treatment2?.setDoctor_id) { + await Treatment2.setDoctor_id(relatedDoctor_id2); + } + + const relatedDoctor_id3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Treatment3 = await Treatments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Treatment3?.setDoctor_id) { + await Treatment3.setDoctor_id(relatedDoctor_id3); + } +} + +async function associateDiagnosisWithClinic() { + const relatedClinic0 = await Clinics.findOne({ + offset: Math.floor(Math.random() * (await Clinics.count())), + }); + const Diagnosis0 = await Diagnoses.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Diagnosis0?.setClinic) { + await Diagnosis0.setClinic(relatedClinic0); + } + + const relatedClinic1 = await Clinics.findOne({ + offset: Math.floor(Math.random() * (await Clinics.count())), + }); + const Diagnosis1 = await Diagnoses.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Diagnosis1?.setClinic) { + await Diagnosis1.setClinic(relatedClinic1); + } + + const relatedClinic2 = await Clinics.findOne({ + offset: Math.floor(Math.random() * (await Clinics.count())), + }); + const Diagnosis2 = await Diagnoses.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Diagnosis2?.setClinic) { + await Diagnosis2.setClinic(relatedClinic2); + } + + const relatedClinic3 = await Clinics.findOne({ + offset: Math.floor(Math.random() * (await Clinics.count())), + }); + const Diagnosis3 = await Diagnoses.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Diagnosis3?.setClinic) { + await Diagnosis3.setClinic(relatedClinic3); + } +} + +async function associateDiagnosisWithTreatment_id() { + const relatedTreatment_id0 = await Treatments.findOne({ + offset: Math.floor(Math.random() * (await Treatments.count())), + }); + const Diagnosis0 = await Diagnoses.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Diagnosis0?.setTreatment_id) { + await Diagnosis0.setTreatment_id(relatedTreatment_id0); + } + + const relatedTreatment_id1 = await Treatments.findOne({ + offset: Math.floor(Math.random() * (await Treatments.count())), + }); + const Diagnosis1 = await Diagnoses.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Diagnosis1?.setTreatment_id) { + await Diagnosis1.setTreatment_id(relatedTreatment_id1); + } + + const relatedTreatment_id2 = await Treatments.findOne({ + offset: Math.floor(Math.random() * (await Treatments.count())), + }); + const Diagnosis2 = await Diagnoses.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Diagnosis2?.setTreatment_id) { + await Diagnosis2.setTreatment_id(relatedTreatment_id2); + } + + const relatedTreatment_id3 = await Treatments.findOne({ + offset: Math.floor(Math.random() * (await Treatments.count())), + }); + const Diagnosis3 = await Diagnoses.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Diagnosis3?.setTreatment_id) { + await Diagnosis3.setTreatment_id(relatedTreatment_id3); + } +} + +async function associatePrescriptionWithClinic() { + const relatedClinic0 = await Clinics.findOne({ + offset: Math.floor(Math.random() * (await Clinics.count())), + }); + const Prescription0 = await Prescriptions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Prescription0?.setClinic) { + await Prescription0.setClinic(relatedClinic0); + } + + const relatedClinic1 = await Clinics.findOne({ + offset: Math.floor(Math.random() * (await Clinics.count())), + }); + const Prescription1 = await Prescriptions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Prescription1?.setClinic) { + await Prescription1.setClinic(relatedClinic1); + } + + const relatedClinic2 = await Clinics.findOne({ + offset: Math.floor(Math.random() * (await Clinics.count())), + }); + const Prescription2 = await Prescriptions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Prescription2?.setClinic) { + await Prescription2.setClinic(relatedClinic2); + } + + const relatedClinic3 = await Clinics.findOne({ + offset: Math.floor(Math.random() * (await Clinics.count())), + }); + const Prescription3 = await Prescriptions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Prescription3?.setClinic) { + await Prescription3.setClinic(relatedClinic3); + } +} + +async function associatePrescriptionWithTreatment_id() { + const relatedTreatment_id0 = await Treatments.findOne({ + offset: Math.floor(Math.random() * (await Treatments.count())), + }); + const Prescription0 = await Prescriptions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Prescription0?.setTreatment_id) { + await Prescription0.setTreatment_id(relatedTreatment_id0); + } + + const relatedTreatment_id1 = await Treatments.findOne({ + offset: Math.floor(Math.random() * (await Treatments.count())), + }); + const Prescription1 = await Prescriptions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Prescription1?.setTreatment_id) { + await Prescription1.setTreatment_id(relatedTreatment_id1); + } + + const relatedTreatment_id2 = await Treatments.findOne({ + offset: Math.floor(Math.random() * (await Treatments.count())), + }); + const Prescription2 = await Prescriptions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Prescription2?.setTreatment_id) { + await Prescription2.setTreatment_id(relatedTreatment_id2); + } + + const relatedTreatment_id3 = await Treatments.findOne({ + offset: Math.floor(Math.random() * (await Treatments.count())), + }); + const Prescription3 = await Prescriptions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Prescription3?.setTreatment_id) { + await Prescription3.setTreatment_id(relatedTreatment_id3); + } +} + module.exports = { up: async (queryInterface, Sequelize) => { await Appointments.bulkCreate(AppointmentsData); @@ -711,6 +1099,10 @@ module.exports = { await Clinics.bulkCreate(ClinicsData); + await Diagnoses.bulkCreate(DiagnosesData); + + await Prescriptions.bulkCreate(PrescriptionsData); + await Promise.all([ // Similar logic for "relation_many" @@ -733,6 +1125,18 @@ module.exports = { await associateTreatmentWithAppointment(), await associateTreatmentWithClinic(), + + await associateTreatmentWithPatient_id(), + + await associateTreatmentWithDoctor_id(), + + await associateDiagnosisWithClinic(), + + await associateDiagnosisWithTreatment_id(), + + await associatePrescriptionWithClinic(), + + await associatePrescriptionWithTreatment_id(), ]); }, @@ -746,5 +1150,9 @@ module.exports = { await queryInterface.bulkDelete('treatments', null, {}); await queryInterface.bulkDelete('clinics', null, {}); + + await queryInterface.bulkDelete('diagnoses', null, {}); + + await queryInterface.bulkDelete('prescriptions', null, {}); }, }; diff --git a/backend/src/db/seeders/20250614121639.js b/backend/src/db/seeders/20250614121639.js new file mode 100644 index 0000000..02dc82a --- /dev/null +++ b/backend/src/db/seeders/20250614121639.js @@ -0,0 +1,87 @@ +const { v4: uuid } = require('uuid'); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = ['diagnoses']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.super_admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/db/seeders/20250614121735.js b/backend/src/db/seeders/20250614121735.js new file mode 100644 index 0000000..856835a --- /dev/null +++ b/backend/src/db/seeders/20250614121735.js @@ -0,0 +1,87 @@ +const { v4: uuid } = require('uuid'); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = ['prescriptions']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.super_admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index 8851051..296b803 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -37,6 +37,10 @@ const permissionsRoutes = require('./routes/permissions'); const clinicsRoutes = require('./routes/clinics'); +const diagnosesRoutes = require('./routes/diagnoses'); + +const prescriptionsRoutes = require('./routes/prescriptions'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -150,6 +154,18 @@ app.use( clinicsRoutes, ); +app.use( + '/api/diagnoses', + passport.authenticate('jwt', { session: false }), + diagnosesRoutes, +); + +app.use( + '/api/prescriptions', + passport.authenticate('jwt', { session: false }), + prescriptionsRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/diagnoses.js b/backend/src/routes/diagnoses.js new file mode 100644 index 0000000..0cc33ae --- /dev/null +++ b/backend/src/routes/diagnoses.js @@ -0,0 +1,455 @@ +const express = require('express'); + +const DiagnosesService = require('../services/diagnoses'); +const DiagnosesDBApi = require('../db/api/diagnoses'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('diagnoses')); + +/** + * @swagger + * components: + * schemas: + * Diagnoses: + * type: object + * properties: + + * code: + * type: string + * default: code + * notes: + * type: string + * default: notes + + */ + +/** + * @swagger + * tags: + * name: Diagnoses + * description: The Diagnoses managing API + */ + +/** + * @swagger + * /api/diagnoses: + * post: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Diagnoses" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Diagnoses" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await DiagnosesService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Diagnoses" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Diagnoses" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await DiagnosesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/diagnoses/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Diagnoses" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Diagnoses" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await DiagnosesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/diagnoses/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Diagnoses" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await DiagnosesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/diagnoses/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Diagnoses" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await DiagnosesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/diagnoses: + * get: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Get all diagnoses + * description: Get all diagnoses + * responses: + * 200: + * description: Diagnoses list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Diagnoses" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await DiagnosesDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'code', 'notes']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/diagnoses/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Count all diagnoses + * description: Count all diagnoses + * responses: + * 200: + * description: Diagnoses count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Diagnoses" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await DiagnosesDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/diagnoses/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Find all diagnoses that match search criteria + * description: Find all diagnoses that match search criteria + * responses: + * 200: + * description: Diagnoses list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Diagnoses" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await DiagnosesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/diagnoses/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Diagnoses] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Diagnoses" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await DiagnosesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/prescriptions.js b/backend/src/routes/prescriptions.js new file mode 100644 index 0000000..403adc3 --- /dev/null +++ b/backend/src/routes/prescriptions.js @@ -0,0 +1,462 @@ +const express = require('express'); + +const PrescriptionsService = require('../services/prescriptions'); +const PrescriptionsDBApi = require('../db/api/prescriptions'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('prescriptions')); + +/** + * @swagger + * components: + * schemas: + * Prescriptions: + * type: object + * properties: + + * medication: + * type: string + * default: medication + * dosage: + * type: string + * default: dosage + * instructions: + * type: string + * default: instructions + + */ + +/** + * @swagger + * tags: + * name: Prescriptions + * description: The Prescriptions managing API + */ + +/** + * @swagger + * /api/prescriptions: + * post: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Prescriptions" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Prescriptions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await PrescriptionsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Prescriptions" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Prescriptions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await PrescriptionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/prescriptions/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Prescriptions" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Prescriptions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await PrescriptionsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/prescriptions/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Prescriptions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await PrescriptionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/prescriptions/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Prescriptions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await PrescriptionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/prescriptions: + * get: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Get all prescriptions + * description: Get all prescriptions + * responses: + * 200: + * description: Prescriptions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Prescriptions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await PrescriptionsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'medication', 'dosage', 'instructions']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/prescriptions/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Count all prescriptions + * description: Count all prescriptions + * responses: + * 200: + * description: Prescriptions count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Prescriptions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await PrescriptionsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/prescriptions/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Find all prescriptions that match search criteria + * description: Find all prescriptions that match search criteria + * responses: + * 200: + * description: Prescriptions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Prescriptions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await PrescriptionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/prescriptions/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Prescriptions] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Prescriptions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await PrescriptionsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/diagnoses.js b/backend/src/services/diagnoses.js new file mode 100644 index 0000000..933f12d --- /dev/null +++ b/backend/src/services/diagnoses.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const DiagnosesDBApi = require('../db/api/diagnoses'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class DiagnosesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await DiagnosesDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await DiagnosesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let diagnoses = await DiagnosesDBApi.findBy({ id }, { transaction }); + + if (!diagnoses) { + throw new ValidationError('diagnosesNotFound'); + } + + const updatedDiagnoses = await DiagnosesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedDiagnoses; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await DiagnosesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await DiagnosesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/prescriptions.js b/backend/src/services/prescriptions.js new file mode 100644 index 0000000..105a6be --- /dev/null +++ b/backend/src/services/prescriptions.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const PrescriptionsDBApi = require('../db/api/prescriptions'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class PrescriptionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await PrescriptionsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await PrescriptionsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let prescriptions = await PrescriptionsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!prescriptions) { + throw new ValidationError('prescriptionsNotFound'); + } + + const updatedPrescriptions = await PrescriptionsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedPrescriptions; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PrescriptionsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PrescriptionsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/search.js b/backend/src/services/search.js index ed3be61..7905c71 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -48,6 +48,10 @@ module.exports = class SearchService { treatments: ['description'], clinics: ['name'], + + diagnoses: ['code', 'notes'], + + prescriptions: ['medication', 'dosage', 'instructions'], }; const columnsInt = { payments: ['amount'], diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Diagnoses/CardDiagnoses.tsx b/frontend/src/components/Diagnoses/CardDiagnoses.tsx new file mode 100644 index 0000000..97e87e5 --- /dev/null +++ b/frontend/src/components/Diagnoses/CardDiagnoses.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + diagnoses: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardDiagnoses = ({ + diagnoses, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_DIAGNOSES'); + + return ( +
+ {loading && } +
    + {!loading && + diagnoses.map((item, index) => ( +
  • +
    + + {item.code} + + +
    + +
    +
    +
    +
    +
    + Treatment_id +
    +
    +
    + {dataFormatter.treatmentsOneListFormatter( + item.treatment_id, + )} +
    +
    +
    + +
    +
    Code
    +
    +
    {item.code}
    +
    +
    + +
    +
    Notes
    +
    +
    {item.notes}
    +
    +
    +
    +
  • + ))} + {!loading && diagnoses.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardDiagnoses; diff --git a/frontend/src/components/Diagnoses/ListDiagnoses.tsx b/frontend/src/components/Diagnoses/ListDiagnoses.tsx new file mode 100644 index 0000000..88a6c83 --- /dev/null +++ b/frontend/src/components/Diagnoses/ListDiagnoses.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + diagnoses: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListDiagnoses = ({ + diagnoses, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_DIAGNOSES'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + diagnoses.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Treatment_id

+

+ {dataFormatter.treatmentsOneListFormatter( + item.treatment_id, + )} +

+
+ +
+

Code

+

{item.code}

+
+ +
+

Notes

+

{item.notes}

+
+ + +
+
+
+ ))} + {!loading && diagnoses.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListDiagnoses; diff --git a/frontend/src/components/Diagnoses/TableDiagnoses.tsx b/frontend/src/components/Diagnoses/TableDiagnoses.tsx new file mode 100644 index 0000000..a006005 --- /dev/null +++ b/frontend/src/components/Diagnoses/TableDiagnoses.tsx @@ -0,0 +1,487 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/diagnoses/diagnosesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureDiagnosesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleDiagnoses = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + diagnoses, + loading, + count, + notify: diagnosesNotify, + refetch, + } = useAppSelector((state) => state.diagnoses); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (diagnosesNotify.showNotification) { + notify( + diagnosesNotify.typeNotification, + diagnosesNotify.textNotification, + ); + } + }, [diagnosesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `diagnoses`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={diagnoses ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleDiagnoses; diff --git a/frontend/src/components/Diagnoses/configureDiagnosesCols.tsx b/frontend/src/components/Diagnoses/configureDiagnosesCols.tsx new file mode 100644 index 0000000..3fd3424 --- /dev/null +++ b/frontend/src/components/Diagnoses/configureDiagnosesCols.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_DIAGNOSES'); + + return [ + { + field: 'treatment_id', + headerName: 'Treatment_id', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('treatments'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'code', + headerName: 'Code', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'notes', + headerName: 'Notes', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Prescriptions/CardPrescriptions.tsx b/frontend/src/components/Prescriptions/CardPrescriptions.tsx new file mode 100644 index 0000000..dbc0f05 --- /dev/null +++ b/frontend/src/components/Prescriptions/CardPrescriptions.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + prescriptions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPrescriptions = ({ + prescriptions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_PRESCRIPTIONS', + ); + + return ( +
+ {loading && } +
    + {!loading && + prescriptions.map((item, index) => ( +
  • +
    + + {item.medication} + + +
    + +
    +
    +
    +
    +
    + Treatment_id +
    +
    +
    + {dataFormatter.treatmentsOneListFormatter( + item.treatment_id, + )} +
    +
    +
    + +
    +
    + Medication +
    +
    +
    + {item.medication} +
    +
    +
    + +
    +
    + Dosage +
    +
    +
    + {item.dosage} +
    +
    +
    + +
    +
    + Instructions +
    +
    +
    + {item.instructions} +
    +
    +
    +
    +
  • + ))} + {!loading && prescriptions.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardPrescriptions; diff --git a/frontend/src/components/Prescriptions/ListPrescriptions.tsx b/frontend/src/components/Prescriptions/ListPrescriptions.tsx new file mode 100644 index 0000000..63bfd68 --- /dev/null +++ b/frontend/src/components/Prescriptions/ListPrescriptions.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + prescriptions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListPrescriptions = ({ + prescriptions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_PRESCRIPTIONS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + prescriptions.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Treatment_id

+

+ {dataFormatter.treatmentsOneListFormatter( + item.treatment_id, + )} +

+
+ +
+

Medication

+

{item.medication}

+
+ +
+

Dosage

+

{item.dosage}

+
+ +
+

Instructions

+

{item.instructions}

+
+ + +
+
+
+ ))} + {!loading && prescriptions.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListPrescriptions; diff --git a/frontend/src/components/Prescriptions/TablePrescriptions.tsx b/frontend/src/components/Prescriptions/TablePrescriptions.tsx new file mode 100644 index 0000000..e1aec23 --- /dev/null +++ b/frontend/src/components/Prescriptions/TablePrescriptions.tsx @@ -0,0 +1,487 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/prescriptions/prescriptionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configurePrescriptionsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSamplePrescriptions = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + prescriptions, + loading, + count, + notify: prescriptionsNotify, + refetch, + } = useAppSelector((state) => state.prescriptions); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (prescriptionsNotify.showNotification) { + notify( + prescriptionsNotify.typeNotification, + prescriptionsNotify.textNotification, + ); + } + }, [prescriptionsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `prescriptions`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={prescriptions ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSamplePrescriptions; diff --git a/frontend/src/components/Prescriptions/configurePrescriptionsCols.tsx b/frontend/src/components/Prescriptions/configurePrescriptionsCols.tsx new file mode 100644 index 0000000..4184aa9 --- /dev/null +++ b/frontend/src/components/Prescriptions/configurePrescriptionsCols.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_PRESCRIPTIONS'); + + return [ + { + field: 'treatment_id', + headerName: 'Treatment_id', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('treatments'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'medication', + headerName: 'Medication', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'dosage', + headerName: 'Dosage', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'instructions', + headerName: 'Instructions', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Treatments/CardTreatments.tsx b/frontend/src/components/Treatments/CardTreatments.tsx index d5d576f..34d2676 100644 --- a/frontend/src/components/Treatments/CardTreatments.tsx +++ b/frontend/src/components/Treatments/CardTreatments.tsx @@ -106,6 +106,28 @@ const CardTreatments = ({
{item.cost}
+ +
+
+ Patient_id +
+
+
+ {dataFormatter.patientsOneListFormatter(item.patient_id)} +
+
+
+ +
+
+ Doctor_id +
+
+
+ {dataFormatter.usersOneListFormatter(item.doctor_id)} +
+
+
))} diff --git a/frontend/src/components/Treatments/ListTreatments.tsx b/frontend/src/components/Treatments/ListTreatments.tsx index 635c628..dd93849 100644 --- a/frontend/src/components/Treatments/ListTreatments.tsx +++ b/frontend/src/components/Treatments/ListTreatments.tsx @@ -71,6 +71,22 @@ const ListTreatments = ({

Cost

{item.cost}

+ +
+

Patient_id

+

+ {dataFormatter.patientsOneListFormatter( + item.patient_id, + )} +

+
+ +
+

Doctor_id

+

+ {dataFormatter.usersOneListFormatter(item.doctor_id)} +

+
value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('patients'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'doctor_id', + headerName: 'Doctor_id', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index 7899651..3bb82c5 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -19,7 +19,7 @@ export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { const style = FooterStyle.WITH_PROJECT_NAME; - const design = FooterDesigns.DESIGN_DIVERSITY; + const design = FooterDesigns.DEFAULT_DESIGN; return (
state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_RIGHT; + const style = HeaderStyle.PAGES_LEFT; - const design = HeaderDesigns.DEFAULT_DESIGN; + const design = HeaderDesigns.DESIGN_DIVERSITY; return (
item.description); + }, + treatmentsOneListFormatter(val) { + if (!val) return ''; + return val.description; + }, + treatmentsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.description }; + }); + }, + treatmentsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.description, id: val.id }; + }, + rolesManyListFormatter(val) { if (!val || !val.length) return []; return val.map((item) => item.name); diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 406ec62..37022fe 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -84,6 +84,22 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiTable ?? icon.mdiTable, permissions: 'READ_CLINICS', }, + { + href: '/diagnoses/diagnoses-list', + label: 'Diagnoses', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_DIAGNOSES', + }, + { + href: '/prescriptions/prescriptions-list', + label: 'Prescriptions', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PRESCRIPTIONS', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/clinics/clinics-view.tsx b/frontend/src/pages/clinics/clinics-view.tsx index 6e2b055..fb3cf7f 100644 --- a/frontend/src/pages/clinics/clinics-view.tsx +++ b/frontend/src/pages/clinics/clinics-view.tsx @@ -347,6 +347,92 @@ const ClinicsView = () => { + <> +

Diagnoses clinics

+ +
+ + + + + + + + + + {clinics.diagnoses_clinics && + Array.isArray(clinics.diagnoses_clinics) && + clinics.diagnoses_clinics.map((item: any) => ( + + router.push( + `/diagnoses/diagnoses-view/?id=${item.id}`, + ) + } + > + + + + + ))} + +
CodeNotes
{item.code}{item.notes}
+
+ {!clinics?.diagnoses_clinics?.length && ( +
No data
+ )} +
+ + + <> +

Prescriptions clinics

+ +
+ + + + + + + + + + + + {clinics.prescriptions_clinics && + Array.isArray(clinics.prescriptions_clinics) && + clinics.prescriptions_clinics.map((item: any) => ( + + router.push( + `/prescriptions/prescriptions-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
MedicationDosageInstructions
{item.medication}{item.dosage}{item.instructions}
+
+ {!clinics?.prescriptions_clinics?.length && ( +
No data
+ )} +
+ + { const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); const [clinics, setClinics] = React.useState(loadingMessage); + const [diagnoses, setDiagnoses] = React.useState(loadingMessage); + const [prescriptions, setPrescriptions] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -57,6 +59,8 @@ const Dashboard = () => { 'roles', 'permissions', 'clinics', + 'diagnoses', + 'prescriptions', ]; const fns = [ setUsers, @@ -67,6 +71,8 @@ const Dashboard = () => { setRoles, setPermissions, setClinics, + setDiagnoses, + setPrescriptions, ]; const requests = entities.map((entity, index) => { @@ -454,6 +460,70 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_DIAGNOSES') && ( + +
+
+
+
+ Diagnoses +
+
+ {diagnoses} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_PRESCRIPTIONS') && ( + +
+
+
+
+ Prescriptions +
+
+ {prescriptions} +
+
+
+ +
+
+
+ + )}
diff --git a/frontend/src/pages/diagnoses/[diagnosesId].tsx b/frontend/src/pages/diagnoses/[diagnosesId].tsx new file mode 100644 index 0000000..22909c5 --- /dev/null +++ b/frontend/src/pages/diagnoses/[diagnosesId].tsx @@ -0,0 +1,162 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/diagnoses/diagnosesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditDiagnoses = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + clinics: null, + + treatment_id: null, + + code: '', + + notes: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { diagnoses } = useAppSelector((state) => state.diagnoses); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { diagnosesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: diagnosesId })); + }, [diagnosesId]); + + useEffect(() => { + if (typeof diagnoses === 'object') { + setInitialValues(diagnoses); + } + }, [diagnoses]); + + useEffect(() => { + if (typeof diagnoses === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = diagnoses[el]), + ); + + setInitialValues(newInitialVal); + } + }, [diagnoses]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: diagnosesId, data })); + await router.push('/diagnoses/diagnoses-list'); + }; + + return ( + <> + + {getPageTitle('Edit diagnoses')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/diagnoses/diagnoses-list')} + /> + + +
+
+
+ + ); +}; + +EditDiagnoses.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditDiagnoses; diff --git a/frontend/src/pages/diagnoses/diagnoses-edit.tsx b/frontend/src/pages/diagnoses/diagnoses-edit.tsx new file mode 100644 index 0000000..1c298b1 --- /dev/null +++ b/frontend/src/pages/diagnoses/diagnoses-edit.tsx @@ -0,0 +1,160 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/diagnoses/diagnosesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditDiagnosesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + clinics: null, + + treatment_id: null, + + code: '', + + notes: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { diagnoses } = useAppSelector((state) => state.diagnoses); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof diagnoses === 'object') { + setInitialValues(diagnoses); + } + }, [diagnoses]); + + useEffect(() => { + if (typeof diagnoses === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = diagnoses[el]), + ); + setInitialValues(newInitialVal); + } + }, [diagnoses]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/diagnoses/diagnoses-list'); + }; + + return ( + <> + + {getPageTitle('Edit diagnoses')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/diagnoses/diagnoses-list')} + /> + + +
+
+
+ + ); +}; + +EditDiagnosesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditDiagnosesPage; diff --git a/frontend/src/pages/diagnoses/diagnoses-list.tsx b/frontend/src/pages/diagnoses/diagnoses-list.tsx new file mode 100644 index 0000000..083d56a --- /dev/null +++ b/frontend/src/pages/diagnoses/diagnoses-list.tsx @@ -0,0 +1,167 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableDiagnoses from '../../components/Diagnoses/TableDiagnoses'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/diagnoses/diagnosesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const DiagnosesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Code', title: 'code' }, + { label: 'Notes', title: 'notes' }, + + { label: 'Treatment_id', title: 'treatment_id' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_DIAGNOSES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getDiagnosesCSV = async () => { + const response = await axios({ + url: '/diagnoses?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'diagnosesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Diagnoses')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +DiagnosesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default DiagnosesTablesPage; diff --git a/frontend/src/pages/diagnoses/diagnoses-new.tsx b/frontend/src/pages/diagnoses/diagnoses-new.tsx new file mode 100644 index 0000000..0764b46 --- /dev/null +++ b/frontend/src/pages/diagnoses/diagnoses-new.tsx @@ -0,0 +1,128 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/diagnoses/diagnosesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + clinics: '', + + treatment_id: '', + + code: '', + + notes: '', +}; + +const DiagnosesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/diagnoses/diagnoses-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/diagnoses/diagnoses-list')} + /> + + +
+
+
+ + ); +}; + +DiagnosesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default DiagnosesNew; diff --git a/frontend/src/pages/diagnoses/diagnoses-table.tsx b/frontend/src/pages/diagnoses/diagnoses-table.tsx new file mode 100644 index 0000000..ad9f053 --- /dev/null +++ b/frontend/src/pages/diagnoses/diagnoses-table.tsx @@ -0,0 +1,166 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableDiagnoses from '../../components/Diagnoses/TableDiagnoses'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/diagnoses/diagnosesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const DiagnosesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Code', title: 'code' }, + { label: 'Notes', title: 'notes' }, + + { label: 'Treatment_id', title: 'treatment_id' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_DIAGNOSES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getDiagnosesCSV = async () => { + const response = await axios({ + url: '/diagnoses?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'diagnosesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Diagnoses')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +DiagnosesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default DiagnosesTablesPage; diff --git a/frontend/src/pages/diagnoses/diagnoses-view.tsx b/frontend/src/pages/diagnoses/diagnoses-view.tsx new file mode 100644 index 0000000..9356e19 --- /dev/null +++ b/frontend/src/pages/diagnoses/diagnoses-view.tsx @@ -0,0 +1,104 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/diagnoses/diagnosesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const DiagnosesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { diagnoses } = useAppSelector((state) => state.diagnoses); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View diagnoses')} + + + + + + +
+

clinics

+ +

{diagnoses?.clinics?.name ?? 'No data'}

+
+ +
+

Treatment_id

+ +

{diagnoses?.treatment_id?.description ?? 'No data'}

+
+ +
+

Code

+

{diagnoses?.code}

+
+ +
+

Notes

+

{diagnoses?.notes}

+
+ + + + router.push('/diagnoses/diagnoses-list')} + /> +
+
+ + ); +}; + +DiagnosesView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default DiagnosesView; diff --git a/frontend/src/pages/patients/patients-view.tsx b/frontend/src/pages/patients/patients-view.tsx index b33247f..71fd35d 100644 --- a/frontend/src/pages/patients/patients-view.tsx +++ b/frontend/src/pages/patients/patients-view.tsx @@ -139,6 +139,43 @@ const PatientsView = () => { + <> +

Treatments Patient_id

+ +
+ + + + + + + + {patients.treatments_patient_id && + Array.isArray(patients.treatments_patient_id) && + patients.treatments_patient_id.map((item: any) => ( + + router.push( + `/treatments/treatments-view/?id=${item.id}`, + ) + } + > + + + ))} + +
Cost
{item.cost}
+
+ {!patients?.treatments_patient_id?.length && ( +
No data
+ )} +
+ + { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + clinics: null, + + treatment_id: null, + + medication: '', + + dosage: '', + + instructions: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { prescriptions } = useAppSelector((state) => state.prescriptions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { prescriptionsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: prescriptionsId })); + }, [prescriptionsId]); + + useEffect(() => { + if (typeof prescriptions === 'object') { + setInitialValues(prescriptions); + } + }, [prescriptions]); + + useEffect(() => { + if (typeof prescriptions === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = prescriptions[el]), + ); + + setInitialValues(newInitialVal); + } + }, [prescriptions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: prescriptionsId, data })); + await router.push('/prescriptions/prescriptions-list'); + }; + + return ( + <> + + {getPageTitle('Edit prescriptions')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/prescriptions/prescriptions-list') + } + /> + + +
+
+
+ + ); +}; + +EditPrescriptions.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPrescriptions; diff --git a/frontend/src/pages/prescriptions/prescriptions-edit.tsx b/frontend/src/pages/prescriptions/prescriptions-edit.tsx new file mode 100644 index 0000000..d0b30d0 --- /dev/null +++ b/frontend/src/pages/prescriptions/prescriptions-edit.tsx @@ -0,0 +1,168 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/prescriptions/prescriptionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditPrescriptionsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + clinics: null, + + treatment_id: null, + + medication: '', + + dosage: '', + + instructions: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { prescriptions } = useAppSelector((state) => state.prescriptions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof prescriptions === 'object') { + setInitialValues(prescriptions); + } + }, [prescriptions]); + + useEffect(() => { + if (typeof prescriptions === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = prescriptions[el]), + ); + setInitialValues(newInitialVal); + } + }, [prescriptions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/prescriptions/prescriptions-list'); + }; + + return ( + <> + + {getPageTitle('Edit prescriptions')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/prescriptions/prescriptions-list') + } + /> + + +
+
+
+ + ); +}; + +EditPrescriptionsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPrescriptionsPage; diff --git a/frontend/src/pages/prescriptions/prescriptions-list.tsx b/frontend/src/pages/prescriptions/prescriptions-list.tsx new file mode 100644 index 0000000..82438f2 --- /dev/null +++ b/frontend/src/pages/prescriptions/prescriptions-list.tsx @@ -0,0 +1,171 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePrescriptions from '../../components/Prescriptions/TablePrescriptions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/prescriptions/prescriptionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PrescriptionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Medication', title: 'medication' }, + { label: 'Dosage', title: 'dosage' }, + { label: 'Instructions', title: 'instructions' }, + + { label: 'Treatment_id', title: 'treatment_id' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PRESCRIPTIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPrescriptionsCSV = async () => { + const response = await axios({ + url: '/prescriptions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'prescriptionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Prescriptions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +PrescriptionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PrescriptionsTablesPage; diff --git a/frontend/src/pages/prescriptions/prescriptions-new.tsx b/frontend/src/pages/prescriptions/prescriptions-new.tsx new file mode 100644 index 0000000..b9c6f6a --- /dev/null +++ b/frontend/src/pages/prescriptions/prescriptions-new.tsx @@ -0,0 +1,136 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/prescriptions/prescriptionsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + clinics: '', + + treatment_id: '', + + medication: '', + + dosage: '', + + instructions: '', +}; + +const PrescriptionsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/prescriptions/prescriptions-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/prescriptions/prescriptions-list') + } + /> + + +
+
+
+ + ); +}; + +PrescriptionsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PrescriptionsNew; diff --git a/frontend/src/pages/prescriptions/prescriptions-table.tsx b/frontend/src/pages/prescriptions/prescriptions-table.tsx new file mode 100644 index 0000000..060d5d0 --- /dev/null +++ b/frontend/src/pages/prescriptions/prescriptions-table.tsx @@ -0,0 +1,170 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePrescriptions from '../../components/Prescriptions/TablePrescriptions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/prescriptions/prescriptionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PrescriptionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Medication', title: 'medication' }, + { label: 'Dosage', title: 'dosage' }, + { label: 'Instructions', title: 'instructions' }, + + { label: 'Treatment_id', title: 'treatment_id' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PRESCRIPTIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPrescriptionsCSV = async () => { + const response = await axios({ + url: '/prescriptions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'prescriptionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Prescriptions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +PrescriptionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PrescriptionsTablesPage; diff --git a/frontend/src/pages/prescriptions/prescriptions-view.tsx b/frontend/src/pages/prescriptions/prescriptions-view.tsx new file mode 100644 index 0000000..4098556 --- /dev/null +++ b/frontend/src/pages/prescriptions/prescriptions-view.tsx @@ -0,0 +1,109 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/prescriptions/prescriptionsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PrescriptionsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { prescriptions } = useAppSelector((state) => state.prescriptions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View prescriptions')} + + + + + + +
+

clinics

+ +

{prescriptions?.clinics?.name ?? 'No data'}

+
+ +
+

Treatment_id

+ +

{prescriptions?.treatment_id?.description ?? 'No data'}

+
+ +
+

Medication

+

{prescriptions?.medication}

+
+ +
+

Dosage

+

{prescriptions?.dosage}

+
+ +
+

Instructions

+

{prescriptions?.instructions}

+
+ + + + router.push('/prescriptions/prescriptions-list')} + /> +
+
+ + ); +}; + +PrescriptionsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PrescriptionsView; diff --git a/frontend/src/pages/treatments/[treatmentsId].tsx b/frontend/src/pages/treatments/[treatmentsId].tsx index 513dfd3..a663ec5 100644 --- a/frontend/src/pages/treatments/[treatmentsId].tsx +++ b/frontend/src/pages/treatments/[treatmentsId].tsx @@ -45,6 +45,10 @@ const EditTreatments = () => { cost: '', clinics: null, + + patient_id: null, + + doctor_id: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -135,6 +139,28 @@ const EditTreatments = () => { > + + + + + + + + diff --git a/frontend/src/pages/treatments/treatments-edit.tsx b/frontend/src/pages/treatments/treatments-edit.tsx index 94e1f54..aadcc34 100644 --- a/frontend/src/pages/treatments/treatments-edit.tsx +++ b/frontend/src/pages/treatments/treatments-edit.tsx @@ -45,6 +45,10 @@ const EditTreatmentsPage = () => { cost: '', clinics: null, + + patient_id: null, + + doctor_id: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -133,6 +137,28 @@ const EditTreatmentsPage = () => { > + + + + + + + + diff --git a/frontend/src/pages/treatments/treatments-list.tsx b/frontend/src/pages/treatments/treatments-list.tsx index a7ce403..75ef7b7 100644 --- a/frontend/src/pages/treatments/treatments-list.tsx +++ b/frontend/src/pages/treatments/treatments-list.tsx @@ -34,6 +34,10 @@ const TreatmentsTablesPage = () => { { label: 'Cost', title: 'cost', number: 'true' }, { label: 'Appointment', title: 'appointment' }, + + { label: 'Patient_id', title: 'patient_id' }, + + { label: 'Doctor_id', title: 'doctor_id' }, ]); const hasCreatePermission = diff --git a/frontend/src/pages/treatments/treatments-new.tsx b/frontend/src/pages/treatments/treatments-new.tsx index 4822f80..7244608 100644 --- a/frontend/src/pages/treatments/treatments-new.tsx +++ b/frontend/src/pages/treatments/treatments-new.tsx @@ -40,6 +40,10 @@ const initialValues = { cost: '', clinics: '', + + patient_id: '', + + doctor_id: '', }; const TreatmentsNew = () => { @@ -101,6 +105,26 @@ const TreatmentsNew = () => { > + + + + + + + + diff --git a/frontend/src/pages/treatments/treatments-table.tsx b/frontend/src/pages/treatments/treatments-table.tsx index e902fe8..1d11c0c 100644 --- a/frontend/src/pages/treatments/treatments-table.tsx +++ b/frontend/src/pages/treatments/treatments-table.tsx @@ -34,6 +34,10 @@ const TreatmentsTablesPage = () => { { label: 'Cost', title: 'cost', number: 'true' }, { label: 'Appointment', title: 'appointment' }, + + { label: 'Patient_id', title: 'patient_id' }, + + { label: 'Doctor_id', title: 'doctor_id' }, ]); const hasCreatePermission = diff --git a/frontend/src/pages/treatments/treatments-view.tsx b/frontend/src/pages/treatments/treatments-view.tsx index 09a4de5..bf46b50 100644 --- a/frontend/src/pages/treatments/treatments-view.tsx +++ b/frontend/src/pages/treatments/treatments-view.tsx @@ -84,6 +84,104 @@ const TreatmentsView = () => {

{treatments?.clinics?.name ?? 'No data'}

+
+

Patient_id

+ +

{treatments?.patient_id?.first_name ?? 'No data'}

+
+ +
+

Doctor_id

+ +

{treatments?.doctor_id?.firstName ?? 'No data'}

+
+ + <> +

Diagnoses Treatment_id

+ +
+ + + + + + + + + + {treatments.diagnoses_treatment_id && + Array.isArray(treatments.diagnoses_treatment_id) && + treatments.diagnoses_treatment_id.map((item: any) => ( + + router.push( + `/diagnoses/diagnoses-view/?id=${item.id}`, + ) + } + > + + + + + ))} + +
CodeNotes
{item.code}{item.notes}
+
+ {!treatments?.diagnoses_treatment_id?.length && ( +
No data
+ )} +
+ + + <> +

Prescriptions Treatment_id

+ +
+ + + + + + + + + + + + {treatments.prescriptions_treatment_id && + Array.isArray(treatments.prescriptions_treatment_id) && + treatments.prescriptions_treatment_id.map((item: any) => ( + + router.push( + `/prescriptions/prescriptions-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
MedicationDosageInstructions
{item.medication}{item.dosage}{item.instructions}
+
+ {!treatments?.prescriptions_treatment_id?.length && ( +
No data
+ )} +
+ + { + <> +

Treatments Doctor_id

+ +
+ + + + + + + + {users.treatments_doctor_id && + Array.isArray(users.treatments_doctor_id) && + users.treatments_doctor_id.map((item: any) => ( + + router.push( + `/treatments/treatments-view/?id=${item.id}`, + ) + } + > + + + ))} + +
Cost
{item.cost}
+
+ {!users?.treatments_doctor_id?.length && ( +
No data
+ )} +
+ + { + const { id, query } = data; + const result = await axios.get(`diagnoses${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'diagnoses/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('diagnoses/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'diagnoses/deleteDiagnoses', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`diagnoses/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'diagnoses/createDiagnoses', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('diagnoses', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'diagnoses/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('diagnoses/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'diagnoses/updateDiagnoses', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`diagnoses/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const diagnosesSlice = createSlice({ + name: 'diagnoses', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.diagnoses = action.payload.rows; + state.count = action.payload.count; + } else { + state.diagnoses = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Diagnoses has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Diagnoses'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Diagnoses'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Diagnoses'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Diagnoses has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = diagnosesSlice.actions; + +export default diagnosesSlice.reducer; diff --git a/frontend/src/stores/prescriptions/prescriptionsSlice.ts b/frontend/src/stores/prescriptions/prescriptionsSlice.ts new file mode 100644 index 0000000..b67cd9e --- /dev/null +++ b/frontend/src/stores/prescriptions/prescriptionsSlice.ts @@ -0,0 +1,250 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + prescriptions: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + prescriptions: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'prescriptions/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `prescriptions${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'prescriptions/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('prescriptions/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'prescriptions/deletePrescriptions', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`prescriptions/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'prescriptions/createPrescriptions', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('prescriptions', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'prescriptions/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('prescriptions/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'prescriptions/updatePrescriptions', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`prescriptions/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const prescriptionsSlice = createSlice({ + name: 'prescriptions', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.prescriptions = action.payload.rows; + state.count = action.payload.count; + } else { + state.prescriptions = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Prescriptions has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Prescriptions'.slice(0, -1)} has been deleted`, + ); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Prescriptions'.slice(0, -1)} has been created`, + ); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Prescriptions'.slice(0, -1)} has been updated`, + ); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Prescriptions has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = prescriptionsSlice.actions; + +export default prescriptionsSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index b256280..1702e71 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -12,6 +12,8 @@ import treatmentsSlice from './treatments/treatmentsSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import clinicsSlice from './clinics/clinicsSlice'; +import diagnosesSlice from './diagnoses/diagnosesSlice'; +import prescriptionsSlice from './prescriptions/prescriptionsSlice'; export const store = configureStore({ reducer: { @@ -28,6 +30,8 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, clinics: clinicsSlice, + diagnoses: diagnosesSlice, + prescriptions: prescriptionsSlice, }, });