diff --git a/.gitignore b/.gitignore index 2dec24d72c..71ac584da7 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ nbproject/project.xml *.sublime-workspace server/config.js .brackets.json -/app/nbproject/private/ \ No newline at end of file +/app/nbproject/private/ +newrelic_agent.log + +newrelic.js diff --git a/app/helpers/html-line-break.js b/app/helpers/html-line-break.js new file mode 100644 index 0000000000..6548762c01 --- /dev/null +++ b/app/helpers/html-line-break.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; +export default Ember.Helper.helper(function([text]) { + if (text !== null && typeof text !== 'undefined') { + return new Ember.Handlebars.SafeString(text.replace(/\n/g, '
')); + } else { + return null; + } +}); \ No newline at end of file diff --git a/app/initializers/i18n.js b/app/initializers/i18n.js index 02511aad45..3f727d9ec2 100644 --- a/app/initializers/i18n.js +++ b/app/initializers/i18n.js @@ -7,5 +7,6 @@ export default { app.inject('route', 'i18n', 'service:i18n'); app.inject('controller', 'i18n', 'service:i18n'); app.inject('mixin', 'i18n', 'service:i18n'); + app.inject('model', 'i18n', 'service:i18n'); } }; \ No newline at end of file diff --git a/app/locales/en/translations.js b/app/locales/en/translations.js index 9c600c99ab..7e994a6d44 100644 --- a/app/locales/en/translations.js +++ b/app/locales/en/translations.js @@ -138,12 +138,15 @@ export default { } }, labels: { + cptcode: 'CPT Code', loading: 'Loading', name: 'Name', patient: 'Patient', quantity: 'Quantity', requested_on: 'Requested On', date: 'Date', + date_of_birth: 'Date of Birth', + date_of_birth_short: 'DoB', date_requested: 'Date Requested', date_completed: 'Date Completed', description: 'Description', @@ -155,12 +158,18 @@ export default { action: 'Action', notes: 'Notes', edit: 'Edit', + image_orders: 'Image Orders', + lab_orders: 'Lab Orders', + patient_history: 'Patient History', imaging_type: 'Imaging Type', result: 'Result', results: 'Results', visit: 'Visit', + requests: 'Requests', + completed: 'Completed', + id: 'Id', + on: 'on', type: 'Type', - id: 'ID', sex: 'Sex', age: 'Age', username: 'Username', @@ -196,17 +205,24 @@ export default { location: 'Location', provider: 'Provider', with: 'With', - all_day: 'All Day' + all_day: 'All Day', + physician: 'Physician', + assisting: 'Assisting', + anesthesia: 'Anesthesia', + procedures: 'Procedures' }, messages: { no_items_found: 'No items found.', + no_history_available: 'No history available.', create_new_record: 'Create a new record?', create_new_user: 'Create a new user?', no_users_found: 'No users found.', are_you_sure_delete: 'Are you sure you wish to delete the user {{user}}?', user_has_been_saved: 'The user has been saved.', user_saved: 'User Saved', + on_behalf_of: 'on behalf of', new_patient_has_to_be_created: 'A new patient needs to be created...Please wait..', + no_notes_available: 'No additional clinical notes are available for this visit.', sorry: 'Sorry, something went wrong...' }, alerts: { @@ -226,6 +242,7 @@ export default { delete: 'Delete', new_user: 'New User', add_value: 'Add Value', + new_note: 'New Note', import: 'Import', load_file: 'Load File', new_request: 'New Request', @@ -504,6 +521,31 @@ export default { new_button: '+ new appointment' } }, + visits: { + edit: { + actions: 'Actions', + edit: 'Edit', + date: 'Date', + authored_by: 'Authored By', + note: 'Note', + notes: 'Notes', + new_note: 'New Note', + visit_information: 'Visit Information', + new_appointment: 'New Appointment', + add_diagnosis: 'Add Diagnosis', + diagnosis: 'Diagnosis', + delete: 'Delete', + procedure: 'Procedure', + procedures: 'Procedures', + new_procedure: 'New Procedure', + labs: 'Labs', + new_lab: 'New Lab', + imaging: 'Imaging', + new_imaging: 'New Imaging', + medication: 'Medication', + new_medication: 'New Medication' + } + }, labs: { section_title: 'Labs', requests_title: 'Lab Requests', @@ -530,5 +572,16 @@ export default { request_saved_title: 'Lab Request Saved', request_saved_message: 'The lab request has been saved.' } + }, + common: { + actions: 'Actions' + }, + patients: { + notes: { + on_behalf_of_label: 'On Behalf Of', + on_behalf_of_copy: 'on behalf of', + please_select_a_visit: 'Please select a visit', + note_label: 'Note' + } } }; diff --git a/app/mixins/patient-notes.js b/app/mixins/patient-notes.js new file mode 100644 index 0000000000..cb13e8cbfd --- /dev/null +++ b/app/mixins/patient-notes.js @@ -0,0 +1,35 @@ +import Ember from 'ember'; +export default Ember.Mixin.create({ + + canAddNote: function() { + return this.currentUserCan('add_note') && (!Ember.isEmpty(this.get('visits')) || !Ember.isEmpty(this.get('model.visits'))); + }, + + canDeleteNote: function() { + return this.currentUserCan('delete_note'); + }, + + _computeNoteType: function(visit) { + switch (visit.get('visitType')) { + case 'Admission': + if (Ember.isEmpty(visit.get('procedures'))) { + return 'Pre-op'; + } else { + return 'Post-op'; + } + break; + case 'Clinic': + case 'Followup': + return 'General'; + default: + return visit.get('visitType'); + } + }, + + _setNoteType: function() { + var model = this.get('model'); + if (model.get('noteType') == null) { + model.set('noteType', this._computeNoteType(model.get('visit'))); + } + } +}); \ No newline at end of file diff --git a/app/mixins/user-session.js b/app/mixins/user-session.js index be01bed744..faa4e285e8 100644 --- a/app/mixins/user-session.js +++ b/app/mixins/user-session.js @@ -437,6 +437,20 @@ export default Ember.Mixin.create({ users: [ 'User Administrator', 'System Administrator' + ], + add_note: [ + 'Doctor', + 'Medical Records Officer', + 'Nurse', + 'Nurse Manager', + 'Patient Administration', + 'System Administrator' + ], + delete_note: [ + 'Medical Records Officer', + 'Nurse Manager', + 'Patient Administration', + 'System Administrator' ] }, diff --git a/app/models/patient-note.js b/app/models/patient-note.js new file mode 100644 index 0000000000..84e3b6c827 --- /dev/null +++ b/app/models/patient-note.js @@ -0,0 +1,42 @@ +import AbstractModel from 'hospitalrun/models/abstract'; +import Ember from 'ember'; +import DS from 'ember-data'; +export default AbstractModel.extend({ + authoredBy: function() { + if (!Ember.isEmpty(this.get('attribution'))) { + let i18n = this.get('i18n'); + return this.get('createdBy') + ' ' + i18n.t('patients.notes.on_behalf_of_copy') + ' ' + this.get('attribution'); + } else { + return this.get('createdBy'); + } + }.property('attribution', 'createdBy'), + // if the note was written by one person but dictated / given on behalf of another, otherwise, this and createdBy are the same + attribution: DS.attr('string'), + content: DS.attr('string'), + createdBy: DS.attr('string'), + date: DS.attr('date'), + // custom list of noteTypes of mixins/patient-note-types + noteType: DS.attr(), + // who is this note about? + patient: DS.belongsTo('patient', { + async: false + }), + // if this note is related to a visit, make sure it's noted. + visit: DS.belongsTo('visit', { + async: false + }), + validations: { + patient: { + presence: true + }, + visit: { + presence: true + }, + noteType: { + presence: true + }, + content: { + presence: true + } + } +}); \ No newline at end of file diff --git a/app/models/visit.js b/app/models/visit.js index 9b26298205..25f1b0f538 100644 --- a/app/models/visit.js +++ b/app/models/visit.js @@ -33,7 +33,9 @@ export default AbstractModel.extend({ labs: DS.hasMany('lab', { async: true }), location: DS.attr('string'), medication: DS.hasMany('medication', { async: true }), + // this field is being deprecated in favor of patient-note notes: DS.attr('string'), + patientNotes: DS.hasMany('patient-note', { async: true }), outPatient: DS.attr('boolean'), patient: DS.belongsTo('patient', { async: false diff --git a/app/patients/edit/controller.js b/app/patients/edit/controller.js index b3b38a8b94..5ef22f5d90 100644 --- a/app/patients/edit/controller.js +++ b/app/patients/edit/controller.js @@ -1,10 +1,11 @@ import AbstractEditController from 'hospitalrun/controllers/abstract-edit-controller'; import BloodTypes from 'hospitalrun/mixins/blood-types'; import Ember from 'ember'; +import PatientNotes from 'hospitalrun/mixins/patient-notes'; import ReturnTo from 'hospitalrun/mixins/return-to'; import SelectValues from 'hospitalrun/utils/select-values'; import UserSession from 'hospitalrun/mixins/user-session'; -export default AbstractEditController.extend(BloodTypes, ReturnTo, UserSession, { +export default AbstractEditController.extend(BloodTypes, ReturnTo, UserSession, PatientNotes, { canAddAppointment: function() { return this.currentUserCan('add_appointment'); }.property(), @@ -284,33 +285,41 @@ export default AbstractEditController.extend(BloodTypes, ReturnTo, UserSession, }, editAppointment: function(appointment) { - appointment.set('returnToPatient', true); - appointment.set('returnTo', null); - this.transitionToRoute('appointments.edit', appointment); + if (this.get('canAddAppointment')) { + appointment.set('returnToPatient', true); + appointment.set('returnTo', null); + this.transitionToRoute('appointments.edit', appointment); + } }, editImaging: function(imaging) { - if (imaging.get('canEdit')) { - imaging.setProperties({ - 'returnToPatient': true - }); - this.transitionToRoute('imaging.edit', imaging); + if (this.get('canAddImaging')) { + if (imaging.get('canEdit')) { + imaging.setProperties({ + 'returnToPatient': true + }); + this.transitionToRoute('imaging.edit', imaging); + } } }, editLab: function(lab) { - if (lab.get('canEdit')) { - lab.setProperties({ - 'returnToPatient': true - }); - this.transitionToRoute('labs.edit', lab); + if (this.get('canAddLab')) { + if (lab.get('canEdit')) { + lab.setProperties({ + 'returnToPatient': true + }); + this.transitionToRoute('labs.edit', lab); + } } }, editMedication: function(medication) { - if (medication.get('canEdit')) { - medication.set('returnToPatient', true); - this.transitionToRoute('medication.edit', medication); + if (this.get('canAddMedication')) { + if (medication.get('canEdit')) { + medication.set('returnToPatient', true); + this.transitionToRoute('medication.edit', medication); + } } }, @@ -319,11 +328,15 @@ export default AbstractEditController.extend(BloodTypes, ReturnTo, UserSession, }, editProcedure: function(procedure) { - this.transitionToRoute('procedures.edit', procedure); + if (this.get('canAddVisit')) { + this.transitionToRoute('procedures.edit', procedure); + } }, editVisit: function(visit) { - this.transitionToRoute('visits.edit', visit); + if (this.get('canAddVisit')) { + this.transitionToRoute('visits.edit', visit); + } }, newAppointment: function() { @@ -358,6 +371,18 @@ export default AbstractEditController.extend(BloodTypes, ReturnTo, UserSession, }); }, + showAddPatientNote: function(model) { + if (this.get('canAddNote')) { + if (Ember.isEmpty(model)) { + model = this.get('store').createRecord('patient-note', { + patient: this.get('model'), + createdBy: this.getUserName() + }); + } + this.send('openModal', 'patients.notes', model); + } + }, + showDeleteAppointment: function(appointment) { appointment.set('deleteFromPatient', true); this.send('openModal', 'appointments.delete', appointment); diff --git a/app/patients/edit/route.js b/app/patients/edit/route.js index 6220189d48..0eb7db4f49 100644 --- a/app/patients/edit/route.js +++ b/app/patients/edit/route.js @@ -2,14 +2,20 @@ import AbstractEditRoute from 'hospitalrun/routes/abstract-edit-route'; import Ember from 'ember'; import PatientId from 'hospitalrun/mixins/patient-id'; import PatientVisits from 'hospitalrun/mixins/patient-visits'; +import PatientNotes from 'hospitalrun/mixins/patient-notes'; import PouchDbMixin from 'hospitalrun/mixins/pouchdb'; -export default AbstractEditRoute.extend(PatientId, PatientVisits, PouchDbMixin, { +export default AbstractEditRoute.extend(PatientId, PatientVisits, PouchDbMixin, PatientNotes, { editTitle: 'Edit Patient', modelName: 'patient', newTitle: 'New Patient', photos: null, actions: { + updateNote: function(note) { + note.get('visit').save().then(function() { + // noop + }); + }, appointmentDeleted: function(model) { this.controller.send('appointmentDeleted', model); }, diff --git a/app/patients/edit/template.hbs b/app/patients/edit/template.hbs index 1296a7970d..95f94bef1f 100644 --- a/app/patients/edit/template.hbs +++ b/app/patients/edit/template.hbs @@ -1,21 +1,130 @@ {{#edit-panel editPanelProps=editPanelProps}} {{#em-form model=model submitButton=false bubbles=false }} {{#unless isNewOrDeleted}} - {{patient-summary patient=model visits=model.visits patientProcedures=patientProcedures disablePatientLink=true }} + {{patient-summary patient=model visits=model.visits patientProcedures=patientProcedures disablePatientLink=true store=store }} {{/unless}}
-
+ {{#unless isNewOrDeleted}} +
+
+ {{#if canAddNote}} +
+ +
+ {{/if}} +
+ {{#if model.visits}} + {{#each model.visits as |visit|}} +
+ +
+ {{visit.visitDate}} + {{visit.visitType}} +
+ + {{#if visit.history}} +
+
History
+ {{html-line-break visit.history}} +
+ {{/if}} + + {{#if visit.historySince}} +
+
History Since
+ {{html-line-break visit.historySince}} +
+ {{/if}} + + {{#if visit.procedures}} +
+
{{t 'labels.procedures' }}
+ {{#each visit.procedures as |procedure|}} +
+
{{date-format procedure.procedureDate}}: + {{#if procedure.cptCode}} + [{{t 'labels.cptcode' }} - {{procedure.cptCode}}] + {{/if}} + {{procedure.description}} +
+
+ {{t 'labels.physician'}}: {{procedure.physician}} + {{#if procedure.assistant}} + , {{t 'labels.assisting'}}: {{procedure.assistant}} + {{/if}} + {{#if procedure.anesthesiologist}} + , {{t 'labels.anesthesia'}}: {{procedure.anesthesiologist}} + {{/if}} +
+ {{html-line-break procedure.notes}} +
+ {{/each}} +
+ {{/if}} + {{#if visit.imaging}} +
+
{{t 'labels.image_orders' }}
+ {{#each visit.imaging as |imaging|}} +
+
{{date-format imaging.imagingDate}}: {{imaging.imagingType.name}}
+
{{imaging.result}}
+
{{html-line-break imaging.notes}}
+
+ {{/each}} +
+ {{/if}} + {{#if visit.labs}} +
+
{{t 'labels.lab_orders' }}
+ {{#each visit.labs as |lab|}} +
+
{{date-format lab.labDate}}: {{lab.labType.name}}
+
{{lab.result}}
+
{{html-line-break lab.notes}}
+
+ {{/each}} +
+ {{/if}} + {{#if visit.patientNotes}} +
+
{{t 'labels.notes'}}
+ {{#each visit.patientNotes as |note|}} +
+
{{note.authoredBy}}
+ {{date-format note.date}}[{{note.noteType}}]: {{html-line-break note.content}} +
+ {{/each}} +
+ {{else}} +
+ {{t 'messages.no_notes_available'}} +
+ {{/if}} +
+ {{/each}} + {{else}} + {{t 'messages.no_history_available'}} + {{/if}} +
+
+
+ {{/unless}} +
@@ -162,48 +271,48 @@
-
-
- {{#if canAddMedication}} -
- -
- {{/if}} -
- {{partial 'patients/medication'}} -
-
-
-
-
- {{#if canAddImaging}} -
- -
- {{/if}} -
- {{partial 'patients/imaging'}} -
-
-
-
+
- {{#if canAddLab}} + {{#if canAddAppointment}}
-
{{/if}}
- {{partial 'patients/labs'}} + + + + + + + + + + {{#each model.appointments as |appointment|}} + + + + + + + + + {{/each}} +
DateExaminerLocationTypeStatus{{t 'labels.actions'}}
{{appointment.formattedAppointmentDate}}{{appointment.provider}}{{appointment.location}}{{appointment.appointmentType}}{{appointment.displayStatus}} + {{#if canAddAppointment}} + + {{/if}} + {{#if canDeleteAppointment}} + + {{/if}} +
-
+
{{#if canAddVisit}} @@ -245,44 +354,46 @@
+
+
+
+ {{#if canAddMedication}} +
+ +
+ {{/if}} +
+ {{partial 'patients/medication'}} +
+
+
+
- {{#if canAddAppointment}} + {{#if canAddImaging}}
-
{{/if}}
- - - - - - - - - - {{#each model.appointments as |appointment|}} - - - - - - - - - {{/each}} -
DateExaminerLocationTypeStatus{{t 'labels.actions'}}
{{appointment.formattedAppointmentDate}}{{appointment.provider}}{{appointment.location}}{{appointment.appointmentType}}{{appointment.displayStatus}} - {{#if canAddAppointment}} - - {{/if}} - {{#if canDeleteAppointment}} - - {{/if}} -
+ {{partial 'patients/imaging'}} +
+
+
+
+
+ {{#if canAddLab}} +
+ +
+ {{/if}} +
+ {{partial 'patients/labs'}}
diff --git a/app/patients/notes/controller.js b/app/patients/notes/controller.js new file mode 100644 index 0000000000..3425fb1871 --- /dev/null +++ b/app/patients/notes/controller.js @@ -0,0 +1,51 @@ +import AbstractEditController from 'hospitalrun/controllers/abstract-edit-controller'; +import Ember from 'ember'; +import IsUpdateDisabled from 'hospitalrun/mixins/is-update-disabled'; +import PatientSubmodule from 'hospitalrun/mixins/patient-submodule'; +import PatientNotes from 'hospitalrun/mixins/patient-notes'; +import UserSession from 'hospitalrun/mixins/user-session'; +export default AbstractEditController.extend(IsUpdateDisabled, UserSession, PatientSubmodule, PatientNotes, { + cancelAction: 'closeModal', + updateAction: 'updateNote', + moduleController: Ember.inject.controller('patients'), + physicianList: Ember.computed.alias('moduleController.physicianList'), + lookupListsToUpdate: [{ + name: 'physicianList', + property: 'model.attribution', + id: 'physician_list' + }], + title: function() { + if (this.get('model.isNew')) { + return 'New Note for ' + this.get('model.patient.displayName'); + } else { + return 'Updating Note from ' + (moment(this.get('model.date')).format('MM/DD/YYYY')) + ' for ' + this.get('model.patient.displayName'); + } + }.property('model.patient.displayName'), + updateCapability: 'add_note', + beforeUpdate: function() { + this.set('model.date', new Date()); + this.set('model.createdBy', this.getUserName()); + return Ember.RSVP.Promise.resolve(); + }, + afterUpdate: function() { + this.send(this.get('updateAction'), this.get('model')); + this.send(this.get('cancelAction')); + }, + actions: { + changeVisit: function() { + const selectEl = $('select[name="note-visits"]')[0]; + const selectedIndex = selectEl.selectedIndex; + const content = this.get('patientVisitsForSelect'); + + // decrement index by 1 if we have a prompt + const contentIndex = selectedIndex - 1; + + const selection = content[contentIndex].selectObject; + + // set the local, shadowed selection to avoid leaking + // changes to `selection` out via 2-way binding + this.get('model').set('visit', selection); + this._setNoteType(); + } + } +}); \ No newline at end of file diff --git a/app/patients/notes/template.hbs b/app/patients/notes/template.hbs new file mode 100644 index 0000000000..361467c33c --- /dev/null +++ b/app/patients/notes/template.hbs @@ -0,0 +1,30 @@ +{{#modal-dialog + hideCancelButton=hideCancelButton + hideUpdateButton=hideUpdateButton + isUpdateDisabled=isUpdateDisabled + title=title + updateButtonAction=updateButtonAction + updateButtonText=updateButtonText }} + {{#em-form model=model submitButton=false }} + {{em-text label=(t 'patients.notes.note_label') property="content" rows=3 class="test-note-content required form-input-group"}} + + + {{select-or-typeahead + property="attribution" + label=(t 'patients.notes.on_behalf_of_label' ) + list=physicianList + selection=attribution + className="form-input-group test-note-attribution" + }} + {{/em-form}} +{{/modal-dialog}} \ No newline at end of file diff --git a/app/styles/_bootstrap-theme.scss b/app/styles/_bootstrap-theme.scss index c92d40f09a..66403606b4 100644 --- a/app/styles/_bootstrap-theme.scss +++ b/app/styles/_bootstrap-theme.scss @@ -285,3 +285,7 @@ .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] { margin-left: -18px; } + +.form-control { + border: 1px solid $blue_lightest2; +} diff --git a/app/styles/_bootstrap.scss b/app/styles/_bootstrap.scss index a12db47b24..507495abf6 100644 --- a/app/styles/_bootstrap.scss +++ b/app/styles/_bootstrap.scss @@ -1766,8 +1766,6 @@ output { background-image: none; border: none; border-radius: 3px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; &:focus { @@ -4157,7 +4155,6 @@ select[multiple].input-group-sm > { padding-left: 0; margin-bottom: 0; list-style: none; - font-weight: 300; font-size: 16px; > li { position: relative; @@ -5674,7 +5671,6 @@ a.list-group-item-danger { .panel-footer { padding: 10px 15px; background-color: #f5f5f5; - border-top: 1px solid #ddd; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } diff --git a/app/styles/_variables_mixins.scss b/app/styles/_variables_mixins.scss index fbd70c90f3..0e7fc2b78f 100644 --- a/app/styles/_variables_mixins.scss +++ b/app/styles/_variables_mixins.scss @@ -29,6 +29,7 @@ $sidebar_icon: #92a7bc; $sidebar_icon_lighter: $navy_light; $sidebar_sublink_text: #c2d2e3; $view_sub_nav: #6784a2; +$content_border: $blue_lightest; // end variables ------------/ diff --git a/app/styles/app.scss b/app/styles/app.scss index 90236c3639..e08a4a9aa8 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -17,6 +17,7 @@ @import 'components/tab_nav'; @import 'components/tab_content'; @import 'components/patient_summary'; +@import 'components/patient_history'; // NOTE: do not place styles in this file // If you have styles to add, put them in diff --git a/app/styles/components/_patient_history.scss b/app/styles/components/_patient_history.scss new file mode 100644 index 0000000000..088513f4bf --- /dev/null +++ b/app/styles/components/_patient_history.scss @@ -0,0 +1,42 @@ +// styles for patient history section +// viewable at Patients > Edit Patient > History +.patient-history-item { + margin: 15px 0; + border-radius: 5px; + background: rgba($blue_lightest,.5); + padding: 5px 15px 15px; + + .ph-note-item { + margin-top: 15px; + font-weight: 300; + } + + .ph-note-heading { + font-size: 16px; + font-weight: 600; + } +} + +.patient-history-heading { + margin: -5px -15px 10px; + border-radius: 5px 5px 0 0; + background: $blue_lightest; + padding: 5px 15px; + font-size: 18px; + + .ph-visit-date { + display: inline-block; + margin: -5px 10px -5px -15px; + border-radius: 5px 0 0; + background: $blue_lightest2; + padding: 5px 15px; + line-height: 26px; + font-size: 16px; + font-weight: 600; + } + + .ph-visit-type { + display: inline-block; + font-weight: 300; + } +} diff --git a/app/styles/components/_patient_summary.scss b/app/styles/components/_patient_summary.scss index ba3dc3962d..f228749f30 100644 --- a/app/styles/components/_patient_summary.scss +++ b/app/styles/components/_patient_summary.scss @@ -1,18 +1,18 @@ // styles for patient summary .patient-summary { position: relative; - border-bottom: 2px solid $blue_lightest; + border-bottom: 2px solid $content_border; padding-bottom: 15px; .ps-info-group { margin-bottom: 5px; &.patient-id { position: absolute; - top: 0; - right: 0; + top: -10px; + right: -10px; border-radius: 3px; - background: $blue_lightest; - padding: 0 10px; + background: rgba($blue_lightest,.7); + padding: 10px 20px; text-align: center; .ps-info-label { @@ -24,7 +24,7 @@ .ps-info-data { font-size: 22px; - font-weight: 600; + font-weight: 300; } } diff --git a/app/styles/components/_sidebar_nav.scss b/app/styles/components/_sidebar_nav.scss index ba22142657..770a59b672 100644 --- a/app/styles/components/_sidebar_nav.scss +++ b/app/styles/components/_sidebar_nav.scss @@ -112,6 +112,7 @@ } input { + border: 0; border-radius: 5px 0 0 5px; background-color: $navy_mid; padding: 6px 12px; diff --git a/app/styles/components/_tab_nav.scss b/app/styles/components/_tab_nav.scss index 34e72ab484..82fd57cd9c 100644 --- a/app/styles/components/_tab_nav.scss +++ b/app/styles/components/_tab_nav.scss @@ -29,7 +29,7 @@ } } - &.tab-nav { border-bottom: 2px solid $blue_lightest; + &.tab-nav { border-bottom: 2px solid $content_border; li { margin-left: 0; diff --git a/app/templates/components/patient-summary.hbs b/app/templates/components/patient-summary.hbs index d38398a9e2..7a87e3948c 100644 --- a/app/templates/components/patient-summary.hbs +++ b/app/templates/components/patient-summary.hbs @@ -17,7 +17,7 @@
- {{patient.age}} + {{patient.age}} ({{date-format patient.dateOfBirth}})
diff --git a/app/visits/edit/controller.js b/app/visits/edit/controller.js index 78554bd516..572e86d5d8 100644 --- a/app/visits/edit/controller.js +++ b/app/visits/edit/controller.js @@ -1,12 +1,13 @@ import AbstractEditController from 'hospitalrun/controllers/abstract-edit-controller'; import ChargeActions from 'hospitalrun/mixins/charge-actions'; import Ember from 'ember'; +import PatientNotes from 'hospitalrun/mixins/patient-notes'; import PatientSubmodule from 'hospitalrun/mixins/patient-submodule'; import SelectValues from 'hospitalrun/utils/select-values'; import UserSession from 'hospitalrun/mixins/user-session'; import VisitTypes from 'hospitalrun/mixins/visit-types'; -export default AbstractEditController.extend(ChargeActions, PatientSubmodule, UserSession, VisitTypes, { +export default AbstractEditController.extend(ChargeActions, PatientSubmodule, PatientNotes, UserSession, VisitTypes, { visitsController: Ember.inject.controller('visits'), canAddAppointment: function() { @@ -272,6 +273,18 @@ export default AbstractEditController.extend(ChargeActions, PatientSubmodule, Us this.send('openModal', 'visits.vitals.edit', newVitals); }, + showAddPatientNote: function(model) { + if (Ember.isEmpty(model)) { + model = this.get('store').createRecord('patient-note', { + visit: this.get('model'), + createdBy: this.getUserName(), + patient: this.get('model').get('patient'), + noteType: this._computeNoteType(this.get('model')) + }); + } + this.send('openModal', 'patients.notes', model); + }, + newAppointment: function() { this._addChildObject('appointments.edit'); }, @@ -328,6 +341,24 @@ export default AbstractEditController.extend(ChargeActions, PatientSubmodule, Us showEditVitals: function(vitals) { this.send('openModal', 'visits.vitals.edit', vitals); + }, + + showDeletePatientNote: function(note) { + this.send('openModal', 'dialog', Ember.Object.create({ + confirmAction: 'deletePatientNote', + title: 'Delete Note', + message: 'Are you sure you want to delete this note?', + noteToDelete: note, + updateButtonAction: 'confirm', + updateButtonText: 'Ok' + })); + }, + + deletePatientNote: function(model) { + var note = model.get('noteToDelete'); + var patientNotes = this.get('model.patientNotes'); + patientNotes.removeObject(note); + this.send('update', true); } } }); diff --git a/app/visits/edit/route.js b/app/visits/edit/route.js index b650e11fce..ccbe9dfeec 100644 --- a/app/visits/edit/route.js +++ b/app/visits/edit/route.js @@ -13,6 +13,14 @@ export default AbstractEditRoute.extend(ChargeRoute, { startDate: new Date(), status: 'Admitted' }); - } + }, -}); + actions: { + updateNote: function() { + this.controller.send('update', true); + }, + deletePatientNote: function(model) { + this.controller.send('deletePatientNote', model); + } + } +}); \ No newline at end of file diff --git a/app/visits/edit/template.hbs b/app/visits/edit/template.hbs index f21e50e17e..3ab30832fc 100644 --- a/app/visits/edit/template.hbs +++ b/app/visits/edit/template.hbs @@ -4,11 +4,11 @@

- Visit Information + {{t 'visits.edit.visit_information' }} {{#if canAddAppointment}} {{/if}}

@@ -60,7 +60,7 @@

- +

{{/if}} @@ -71,9 +71,9 @@ - - - + + + {{#each model.additionalDiagnoses as |diagnosis|}} @@ -82,7 +82,7 @@ @@ -92,7 +92,6 @@ {{/if}} {{em-text label="Patient History" property="history" rows=3 }} {{em-text label="History since last seen" property="historySince" rows=3 }} - {{em-text label="Notes" property="notes" rows=3 }} {{/em-form}} @@ -102,9 +101,50 @@ +
+
+
DateDiagnosisDelete{{t 'visits.edit.date' }}{{t 'visits.edit.diagnosis' }}{{t 'visits.edit.actions' }}
{{#if canDeleteDiagnosis}} {{/if}}
+ + + + + + + {{#each model.patientNotes as |note|}} + + + + + + + {{/each}} +
{{t 'visits.edit.date'}}{{t 'visits.edit.authored_by'}}{{t 'visits.edit.note'}}{{t 'common.actions'}}
{{date-format note.date}}{{note.authoredBy}}{{note.noteType}}: {{note.content}} + {{#if canAddNote}} + + {{/if}} + {{#if canDeleteNote}} + + {{/if}} +
+
+
+
+
+
+

+ + {{t 'visits.edit.procedures' }} {{#if canAddProcedure}} - + {{/if}}

@@ -113,9 +153,9 @@
- - - + + + {{#each model.procedures as |procedure|}} @@ -127,7 +167,7 @@ {{/if}} {{#if canDeleteProcedure}} {{/if}} @@ -141,9 +181,9 @@

- Medication + {{t 'visits.edit.medication' }} {{#if canAddMedication}} - + {{/if}}

@@ -158,10 +198,10 @@

- Labs + {{t 'visits.edit.labs' }} {{#if canAddLab}} {{/if}} @@ -177,10 +217,10 @@

- Imaging + {{t 'visits.edit.imaging' }} {{#if canAddImaging}} {{/if}} diff --git a/config/environment.js b/config/environment.js index e467a343b0..7a3e2b5948 100644 --- a/config/environment.js +++ b/config/environment.js @@ -19,12 +19,14 @@ module.exports = function(environment) { } }; + ENV.contentSecurityPolicy = { - 'script-src': "'self' 'unsafe-inline' 'unsafe-eval'", 'connect-src': "'self'", + 'default-src': "'self'", 'frame-src': "'self'", - 'style-src': "'self' 'unsafe-inline'", - 'img-src': "'self' data:" + 'img-src': "'self' filesystem: data:", + 'script-src': "'self' 'unsafe-inline' 'unsafe-eval'", + 'style-src': "'self' 'unsafe-inline'" }; if (environment === 'test') { diff --git a/tests/acceptance/appointments-test.js b/tests/acceptance/appointments-test.js index 488f7eaf3f..3b66d62975 100644 --- a/tests/acceptance/appointments-test.js +++ b/tests/acceptance/appointments-test.js @@ -85,6 +85,7 @@ test('Adding a visit to an appointment', function(assert) { }); click('button:contains(Ok)'); andThen(() => { + findWithAssert('button:contains(New Note)'); findWithAssert('button:contains(New Procedure)'); findWithAssert('button:contains(New Medication)'); findWithAssert('button:contains(New Lab)'); diff --git a/tests/acceptance/patient-notes-test.js b/tests/acceptance/patient-notes-test.js new file mode 100644 index 0000000000..c709667428 --- /dev/null +++ b/tests/acceptance/patient-notes-test.js @@ -0,0 +1,106 @@ +import Ember from 'ember'; +import { module, test } from 'qunit'; +import startApp from 'hospitalrun/tests/helpers/start-app'; + +module('Acceptance | patient notes', { + beforeEach: function() { + this.application = startApp(); + }, + + afterEach: function() { + Ember.run(this.application, 'destroy'); + } +}); + +function tabTest(tabName, tabId) { + click(`[data-test-selector=${tabName}]`); + andThen(function() { + findWithAssert(`#${tabId}`); + }); +} + +test('patient notes crud testing', function(assert) { + runWithPouchDump('default', function() { + authenticateUser(); + visit('/patients'); + visit('/patients/edit/new'); + andThen(function() { + assert.equal(currentURL(), '/patients/edit/new'); + }); + fillIn('.test-first-name input', 'John'); + fillIn('.test-last-name input', 'Doe'); + click('.panel-footer button:contains(Add)'); + waitToAppear('.modal-dialog'); + andThen(function() { + assert.equal(find('.modal-title').text(), 'Patient Saved', 'Patient record has been saved'); + }); + click('button:contains(Close)'); + waitToAppear('.patient-summary'); + andThen(function() { + findWithAssert('.patient-summary'); + }); + andThen(function() { + tabTest('visits-tab', 'visits'); + }); + click('button:contains(New Visit)'); + andThen(function() { + assert.equal(currentURL(), '/visits/edit/new', 'Now in add visiting information route'); + }); + click('.panel-footer button:contains(Add)'); + waitToAppear('.modal-dialog'); + andThen(() => { + assert.equal(find('.modal-title').text(), 'Visit Saved', 'New visit has been saved'); + }); + click('button:contains(Ok)'); + andThen(() => { + findWithAssert('button:contains(New Note)'); + findWithAssert('button:contains(New Procedure)'); + findWithAssert('button:contains(New Medication)'); + findWithAssert('button:contains(New Lab)'); + findWithAssert('button:contains(New Imaging)'); + findWithAssert('button:contains(New Vitals)'); + findWithAssert('button:contains(Add Item)'); + }); + andThen(function() { + assert.equal(find('button:contains(New Note)').length, 1, 'New Note button in visible'); + click('button:contains(New Note)'); + }); + andThen(function() { + assert.equal(find('label:contains(Note)').length, 1, 'Notes modal appeared.'); + }); + andThen(function() { + fillIn('.test-note-content textarea', 'This is a note.'); + fillIn('.test-note-attribution input', 'Dr. Nick'); + click('.modal-footer button:contains(Add)'); + }); + andThen(function() { + waitToAppear('#visit-notes table tr td:contains(This is a note.)'); + assert.equal(find('#visit-notes table tr td:contains(This is a note.)').length, 1, 'Successfully added note.'); + }); + // update note + andThen(function() { + click('#visit-notes table tr td button:contains(Edit)'); + waitToAppear('.modal-dialog'); + }); + andThen(function() { + fillIn('.test-note-content textarea', 'This is an updated note.'); + click('.modal-footer button:contains(Update)'); + }); + andThen(function() { + waitToAppear('#visit-notes table tr td:contains(This is an updated note.)'); + assert.equal(find('#visit-notes table tr td:contains(This is an updated note.)').length, 1, 'Successfully updated note.'); + }); + // delete note + andThen(function() { + waitToAppear('#visit-notes table tr td'); + click('#visit-notes table tr td button:contains(Delete)'); + }); + andThen(function() { + waitToAppear('.modal-dialog'); + click('.modal-footer button:contains(Ok)'); + }); + andThen(function() { + assert.equal(find('#visit-notes table tr td:contains(This is an updated note.)').length, 0, 'Successfully deleted note.'); + }); + }); +}); \ No newline at end of file diff --git a/tests/acceptance/patients-test.js b/tests/acceptance/patients-test.js index 2bb99d24d6..e7417ede3d 100644 --- a/tests/acceptance/patients-test.js +++ b/tests/acceptance/patients-test.js @@ -98,13 +98,6 @@ test('Adding a new patient record', function(assert) { }); }); -/*function tabTest(tabName, tabTitle) { - click(`[data-test-selector=${tabName}]`); - andThen(function() { - findWithAssert(`[data-test-selector=${tabName}] .active`); - }); -}*/ - function testSimpleReportForm(reportName) { test(`View reports tab | ${reportName} shows start and end dates`, function(assert) { runWithPouchDump('default', function() {

DateProcedure{{t 'labels.actions'}}{{t 'visits.edit.date' }}{{t 'visits.edit.procedure' }}{{t 'common.actions'}}