Skip to content

Commit

Permalink
feat: custom fields consultation (#551)
Browse files Browse the repository at this point in the history
* feat: up 1

* fix: up

* chore: up

* chore: up

* fix: up

* fix: last step

* fix: up

* fix: up

* fix: up

* fix: up

* fix: up
  • Loading branch information
rap2hpoutre authored Apr 1, 2022
1 parent 05918f4 commit ffee784
Show file tree
Hide file tree
Showing 15 changed files with 371 additions and 49 deletions.
64 changes: 64 additions & 0 deletions api/src/controllers/consultation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const express = require("express");
const router = express.Router();
const passport = require("passport");
const { z } = require("zod");
const sequelize = require("../db/sequelize");
const { catchErrors } = require("../errors");
const Organisation = require("../models/organisation");
const validateEncryptionAndMigrations = require("../middleware/validateEncryptionAndMigrations");
const { capture } = require("../sentry");
const validateUser = require("../middleware/validateUser");
const { looseUuidRegex, customFieldSchema } = require("../utils");
const Person = require("../models/person");

router.put(
"/",
passport.authenticate("user", { session: false }),
validateUser("admin"),
validateEncryptionAndMigrations,
catchErrors(async (req, res, next) => {
try {
z.array(
z.object({
_id: z.string().regex(looseUuidRegex),
encrypted: z.string(),
encryptedEntityKey: z.string(),
})
).parse(req.body.persons);
z.optional(
z.array(
z.object({
name: z.string().min(1),
fields: z.array(customFieldSchema),
})
)
).parse(req.body.consultations);
} catch (e) {
const error = new Error(`Invalid request in consultation update: ${e}`);
error.status = 400;
return next(error);
}

const organisation = await Organisation.findOne({ where: { _id: req.user.organisation } });
if (!organisation) return res.status(404).send({ ok: false, error: "Not Found" });

const { persons = [], consulations = [] } = req.body;

try {
await sequelize.transaction(async (tx) => {
for (const { encrypted, encryptedEntityKey, _id } of persons) {
await Person.update({ encrypted, encryptedEntityKey }, { where: { _id }, transaction: tx });
}

organisation.set({ consulations });
await organisation.save({ transaction: tx });
});
} catch (e) {
capture("error updating consultation", e);
throw e;
}
return res.status(200).send({ ok: true });
})
);

module.exports = router;
1 change: 1 addition & 0 deletions api/src/controllers/migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ router.put(
encryptionLastUpdateAt: organisation.encryptionLastUpdateAt,
receptionEnabled: organisation.receptionEnabled,
services: organisation.services,
consultations: organisation.consultations,
collaborations: organisation.collaborations,
customFieldsObs: organisation.customFieldsObs,
encryptedVerificationKey: organisation.encryptedVerificationKey,
Expand Down
25 changes: 12 additions & 13 deletions api/src/controllers/organisation.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const Comment = require("../models/comment");
const Passage = require("../models/passage");
const mailservice = require("../utils/mailservice");
const validateUser = require("../middleware/validateUser");
const { looseUuidRegex } = require("../utils");
const { looseUuidRegex, customFieldSchema } = require("../utils");
const { capture } = require("../sentry");

const JWT_MAX_AGE = 60 * 60 * 3; // 3 hours in s
Expand Down Expand Up @@ -140,21 +140,18 @@ router.put(
z.optional(z.string().min(1)).parse(req.body.name);
z.optional(z.array(z.string().min(1))).parse(req.body.categories);
z.optional(z.array(z.string().min(1))).parse(req.body.collaborations);
const customFieldSchema = z
.object({
name: z.string().min(1),
type: z.string().min(1),
label: z.optional(z.string().min(1)),
enabled: z.optional(z.boolean()),
required: z.optional(z.boolean()),
showInStats: z.optional(z.boolean()),
onlyHealthcareProfessional: z.optional(z.boolean()),
options: z.optional(z.array(z.string())),
})
.strict();
z.optional(z.array(customFieldSchema)).parse(req.body.customFieldsObs);
z.optional(z.array(customFieldSchema)).parse(req.body.customFieldsPersonsSocial);
z.optional(z.array(customFieldSchema)).parse(req.body.customFieldsPersonsMedical);
z.optional(
z.array(
z.object({
name: z.string().min(1),
fields: z.array(customFieldSchema),
})
)
).parse(req.body.consultations);

z.optional(z.string().min(1)).parse(req.body.encryptedVerificationKey);
z.optional(z.boolean()).parse(req.body.encryptionEnabled);
if (req.body.encryptionLastUpdateAt) z.preprocess((input) => new Date(input), z.date()).parse(req.body.encryptionLastUpdateAt);
Expand Down Expand Up @@ -193,6 +190,8 @@ router.put(
typeof req.body.customFieldsPersonsMedical === "string"
? JSON.parse(req.body.customFieldsPersonsMedical)
: req.body.customFieldsPersonsMedical;
if (req.body.hasOwnProperty("consultations"))
updateOrg.consultations = typeof req.body.consultations === "string" ? JSON.parse(req.body.consultations) : req.body.consultations;
if (req.body.hasOwnProperty("encryptedVerificationKey")) updateOrg.encryptedVerificationKey = req.body.encryptedVerificationKey;
if (req.body.hasOwnProperty("encryptionEnabled")) updateOrg.encryptionEnabled = req.body.encryptionEnabled;
if (req.body.hasOwnProperty("encryptionLastUpdateAt")) updateOrg.encryptionLastUpdateAt = req.body.encryptionLastUpdateAt;
Expand Down
1 change: 1 addition & 0 deletions api/src/controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ function serializeUserWithTeamsAndOrganisation(user, teams, organisation) {
createdAt: organisation.createdAt,
updatedAt: organisation.updatedAt,
categories: organisation.categories,
consultations: organisation.consultations,
encryptionEnabled: organisation.encryptionEnabled,
encryptionLastUpdateAt: organisation.encryptionLastUpdateAt,
receptionEnabled: organisation.receptionEnabled,
Expand Down
13 changes: 13 additions & 0 deletions api/src/db/migrations/2022-03-29_add_consultations_columns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const sequelize = require("../sequelize");

const json = JSON.stringify([
{ name: "Psychologique", fields: [{ name: "description", type: "textarea", label: "Description", enabled: true, showInStats: false }] },
{ name: "Infirmier", fields: [{ name: "description", type: "textarea", label: "Description", enabled: true, showInStats: false }] },
{ name: "Médicale", fields: [{ name: "description", type: "textarea", label: "Description", enabled: true, showInStats: false }] },
]);

// No injection here, no need to escape.
sequelize.query(`
ALTER TABLE "mano"."Organisation"
ADD COLUMN IF NOT EXISTS "consultations" JSONB DEFAULT '${json}'::JSONB;
`);
1 change: 1 addition & 0 deletions api/src/db/migrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ require("./2022-03-14_add_passage_table");
require("./2022-03-16-migrating");
require("./2022-03-17_user_healthcare_professional.js");
require("./2022-03-21-debug-app-dashboard-user.js");
require("./2022-03-29_add_consultations_columns.js");
1 change: 1 addition & 0 deletions api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ app.use("/encrypt", require("./controllers/encrypt"));
app.use("/category", require("./controllers/category"));
app.use("/service", require("./controllers/service"));
app.use("/migration", require("./controllers/migration"));
app.use("/consultation", require("./controllers/consultation"));

app.use(errors.sendError);

Expand Down
1 change: 1 addition & 0 deletions api/src/models/organisation.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Organisation.init(
name: DataTypes.TEXT,
categories: DataTypes.ARRAY(DataTypes.TEXT),
collaborations: { type: [DataTypes.ARRAY(DataTypes.TEXT)], defaultValue: [] },
consultations: DataTypes.JSONB,
encryptionEnabled: { type: DataTypes.BOOLEAN },
encryptionLastUpdateAt: DataTypes.DATE,
encryptedVerificationKey: DataTypes.TEXT,
Expand Down
15 changes: 15 additions & 0 deletions api/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const passwordValidator = require("password-validator");
const bcrypt = require("bcryptjs");
const { z } = require("zod");

function validatePassword(password) {
const schema = new passwordValidator();
Expand Down Expand Up @@ -31,6 +32,19 @@ const cryptoHexRegex = /^[A-Fa-f0-9]{16,128}$/;
const positiveIntegerRegex = /^\d+$/;
const jwtRegex = /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/;

const customFieldSchema = z
.object({
name: z.string().min(1),
type: z.string().min(1),
label: z.optional(z.string().min(1)),
enabled: z.optional(z.boolean()),
required: z.optional(z.boolean()),
showInStats: z.optional(z.boolean()),
onlyHealthcareProfessional: z.optional(z.boolean()),
options: z.optional(z.array(z.string())),
})
.strict();

module.exports = {
validatePassword,
comparePassword,
Expand All @@ -39,4 +53,5 @@ module.exports = {
positiveIntegerRegex,
cryptoHexRegex,
jwtRegex,
customFieldSchema,
};
45 changes: 28 additions & 17 deletions dashboard/src/components/TableCustomFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ const sanitizeFields = (field) => {
return sanitizedField;
};

const TableCustomFields = ({ data, customFields, showHealthcareProfessionnalColumn = false }) => {
const TableCustomFields = ({
data,
customFields,
showHealthcareProfessionnalColumn = false,
mergeData = null,
extractData = null,
keyPrefix = null,
hideStats = false,
}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [mutableData, setMutableData] = useState(data);
const [editingField, setEditingField] = useState(null);
Expand Down Expand Up @@ -79,12 +87,12 @@ const TableCustomFields = ({ data, customFields, showHealthcareProfessionnalColu
try {
const response = await API.put({
path: `/organisation/${organisation._id}`,
body: { [customFields]: newData },
body: { [customFields]: mergeData ? mergeData(newData) : newData },
});
if (response.ok) {
toastr.success('Mise à jour !');
setMutableData(extractData ? extractData(response.data[customFields]) : response.data[customFields]);
setOrganisation(response.data);
setMutableData(response.data[customFields]);
}
} catch (orgUpdateError) {
console.log('error in updating organisation', orgUpdateError);
Expand All @@ -96,14 +104,15 @@ const TableCustomFields = ({ data, customFields, showHealthcareProfessionnalColu
const handleSort = async (keys) => {
setIsSubmitting(true);
try {
const dataForApi = keys.map((key) => mutableData.find((field) => field.name === key));
const response = await API.put({
path: `/organisation/${organisation._id}`,
body: { [customFields]: keys.map((key) => mutableData.find((field) => field.name === key)) },
body: { [customFields]: mergeData ? mergeData(dataForApi) : dataForApi },
});
if (response.ok) {
toastr.success('Mise à jour !');
setMutableData(extractData ? extractData(response.data[customFields]) : response.data[customFields]);
setOrganisation(response.data);
setMutableData(response.data[customFields]);
}
} catch (orgUpdateError) {
console.log('error in updating organisation', orgUpdateError);
Expand All @@ -117,7 +126,7 @@ const TableCustomFields = ({ data, customFields, showHealthcareProfessionnalColu
<Table
data={mutableData}
// use this key prop to reset table and reset sortablejs on each element added/removed
key={mutableData.length}
key={(keyPrefix || customFields) + mutableData.length}
rowKey="name"
isSortable
onSort={handleSort}
Expand Down Expand Up @@ -169,17 +178,19 @@ const TableCustomFields = ({ data, customFields, showHealthcareProfessionnalColu
dataKey: 'onlyHealthcareProfessional',
render: (f) => <input type="checkbox" checked={f.onlyHealthcareProfessional} onChange={onOnlyHealthcareProfessionalChange(f)} />,
},
{
title: (
<>
Voir dans les
<br />
statistiques
</>
),
dataKey: 'showInStats',
render: (f) => <input type="checkbox" checked={f.showInStats} onChange={onShowStatsChange(f)} />,
},
hideStats
? null
: {
title: (
<>
Voir dans les
<br />
statistiques
</>
),
dataKey: 'showInStats',
render: (f) => <input type="checkbox" checked={f.showInStats} onChange={onShowStatsChange(f)} />,
},
{
title: '',
dataKey: 'name',
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/recoil/persons.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ export const outOfActiveListReasonOptions = [
'Autre',
];

export const consultationTypes = ['Psychologique', 'Infirmier', 'Médicale'];

export const defaultMedicalCustomFields = [
{
name: 'consumptions',
Expand Down
Loading

0 comments on commit ffee784

Please sign in to comment.