diff --git a/ehr/resources/queries/study/demographicsAge.sql b/ehr/resources/queries/study/demographicsAge.sql index 10d5417c2..150e825d1 100644 --- a/ehr/resources/queries/study/demographicsAge.sql +++ b/ehr/resources/queries/study/demographicsAge.sql @@ -17,15 +17,15 @@ ROUND(CONVERT(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), DOUBLE ROUND(CONVERT(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), DOUBLE) / 12, 1) AS ageInYears, -TIMESTAMPDIFF('SQL_TSI_DAY', d.birth, COALESCE(d.lastDayAtCenter, now())) as ageInDays, +age_in_days(d.birth, COALESCE(d.lastDayAtCenter, now())) as ageInDays, case when (age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now()))) < 1 - then (CONVERT(CONVERT(TIMESTAMPDIFF('SQL_TSI_DAY', d.birth, COALESCE(d.lastDayAtCenter, now())), float), VARCHAR) || ' days') + then (CONVERT(CONVERT(age_in_days(d.birth, COALESCE(d.lastDayAtCenter, now())), float), VARCHAR) || ' days') when (age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now()))) < 12 then (CONVERT(CONVERT(ROUND(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), 1), float), VARCHAR) || ' months') else - (CONVERT(CONVERT(FLOOR(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())) / 12), SQL_INTEGER), VARCHAR) || '.' || CONVERT(MOD(CONVERT(ROUND(age_in_months(d.birth, COALESCE(d.death, now())) / 12.0 * 10.0, 0), SQL_INTEGER), 10), VARCHAR) || ' years') + (CONVERT(CONVERT(FLOOR(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())) / 12), SQL_INTEGER), VARCHAR) || '.' || CONVERT(MOD(CONVERT(ROUND(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())) / 12.0 * 10.0, 0), SQL_INTEGER), 10), VARCHAR) || ' years') end as ageFriendly FROM study.Demographics d diff --git a/ehr/resources/schemas/dbscripts/postgresql/ehr_lookups-26.002-26.003.sql b/ehr/resources/schemas/dbscripts/postgresql/ehr_lookups-26.002-26.003.sql new file mode 100644 index 000000000..5368f65ed --- /dev/null +++ b/ehr/resources/schemas/dbscripts/postgresql/ehr_lookups-26.002-26.003.sql @@ -0,0 +1,60 @@ +-- Dropping ix_ehr_lookups_request_priority_container [container] because it overlaps with uq_request_priority [container, priority] +DROP INDEX ehr_lookups.ix_ehr_lookups_request_priority_container; +-- Dropping ix_ehr_lookups_usda_levels_container [container] because it overlaps with uq_usda_levels [container, usda_level] +DROP INDEX ehr_lookups.ix_ehr_lookups_usda_levels_container; +-- Dropping ix_rooms_container [container] because it overlaps with uq_rooms [container, room] +DROP INDEX ehr_lookups.ix_rooms_container; +-- Dropping ix_ehr_lookups_cage_positions_container [container] because it overlaps with uq_cage_positions [container, cage] +DROP INDEX ehr_lookups.ix_ehr_lookups_cage_positions_container; +-- Dropping ix_ehr_lookups_blood_tube_volumes_container [container] because it overlaps with uq_blood_tube_volumes [container, volume] +DROP INDEX ehr_lookups.ix_ehr_lookups_blood_tube_volumes_container; +-- Dropping ix_ehr_lookups_areas_container [container] because it overlaps with uq_areas [container, area] +DROP INDEX ehr_lookups.ix_ehr_lookups_areas_container; +-- Dropping ix_ehr_lookups_source_container [container] because it overlaps with uq_source [container, code] +DROP INDEX ehr_lookups.ix_ehr_lookups_source_container; +-- Dropping ix_ehr_lookups_flag_categories_container [container] because it overlaps with uq_flag_categories [container, category] +DROP INDEX ehr_lookups.ix_ehr_lookups_flag_categories_container; +-- Dropping ix_ehr_lookups_routes_container [container] because it overlaps with uq_routes [container, route] +DROP INDEX ehr_lookups.ix_ehr_lookups_routes_container; +-- Dropping ix_ehr_lookups_cage_container [container] because it overlaps with uq_cage [container, location] +DROP INDEX ehr_lookups.ix_ehr_lookups_cage_container; +-- Dropping ix_ehr_lookups_blood_draw_services_container [container] because it overlaps with uq_blood_draw_services [container, service] +DROP INDEX ehr_lookups.ix_ehr_lookups_blood_draw_services_container; +-- Dropping ix_ehr_lookups_clinpath_tests_container [container] because it overlaps with uq_clinpath_tests [container, testname] +DROP INDEX ehr_lookups.ix_ehr_lookups_clinpath_tests_container; +-- Dropping ix_ehr_lookups_species_codes_container [container] because it overlaps with uq_species_codes [container, code] +DROP INDEX ehr_lookups.ix_ehr_lookups_species_codes_container; +-- Dropping ix_ehr_lookups_labwork_types_container [container] because it overlaps with uq_labwork_types [container, type] +DROP INDEX ehr_lookups.ix_ehr_lookups_labwork_types_container; +-- Dropping ix_ehr_lookups_death_remarks_container [container] because it overlaps with uq_death_remarks [container, title] +DROP INDEX ehr_lookups.ix_ehr_lookups_death_remarks_container; +-- Dropping ix_ehr_lookups_gender_codes_container [container] because it overlaps with uq_gender_codes [container, code] +DROP INDEX ehr_lookups.ix_ehr_lookups_gender_codes_container; +-- Dropping ix_buildings_container [container] because it overlaps with uq_buildings [container, name] +DROP INDEX ehr_lookups.ix_buildings_container; +-- Dropping ix_ehr_lookups_restraint_type_container [container] because it overlaps with uq_restraint_type [container, type] +DROP INDEX ehr_lookups.ix_ehr_lookups_restraint_type_container; +-- Dropping ix_ehr_lookups_conc_units_container [container] because it overlaps with uq_conc_units [container, unit] +DROP INDEX ehr_lookups.ix_ehr_lookups_conc_units_container; +-- Dropping ix_ehr_lookups_weight_ranges_container [container] because it overlaps with uq_weight_ranges [container, species] +DROP INDEX ehr_lookups.ix_ehr_lookups_weight_ranges_container; +-- Dropping ix_ehr_lookups_dosage_units_container [container] because it overlaps with uq_dosage_units [container, unit] +DROP INDEX ehr_lookups.ix_ehr_lookups_dosage_units_container; +-- Dropping ix_ehr_lookups_volume_units_container [container] because it overlaps with uq_volume_units [container, unit] +DROP INDEX ehr_lookups.ix_ehr_lookups_volume_units_container; +-- Dropping ix_ehr_lookups_blood_draw_tube_type_container [container] because it overlaps with uq_blood_draw_tube_type [container, type] +DROP INDEX ehr_lookups.ix_ehr_lookups_blood_draw_tube_type_container; +-- Dropping ix_ehr_lookups_cage_type_container [container] because it overlaps with uq_cage_type [container, cagetype] +DROP INDEX ehr_lookups.ix_ehr_lookups_cage_type_container; +-- Dropping ix_ehr_lookups_amount_units_container [container] because it overlaps with uq_amount_units [container, unit] +DROP INDEX ehr_lookups.ix_ehr_lookups_amount_units_container; +-- Dropping ix_treatment_codes_container [container] because it overlaps with uq_treatment_codes [container, meaning] +DROP INDEX ehr_lookups.ix_treatment_codes_container; +-- Dropping ix_ehr_lookups_calculated_status_codes_container [container] because it overlaps with uq_calculated_status_codes [container, code] +DROP INDEX ehr_lookups.ix_ehr_lookups_calculated_status_codes_container; +-- Dropping ix_ehr_lookups_species_container [container] because it overlaps with uq_species [container, common] +DROP INDEX ehr_lookups.ix_ehr_lookups_species_container; +-- Dropping ix_ehr_lookups_labwork_services_container [container] because it overlaps with uq_labwork_services [container, servicename] +DROP INDEX ehr_lookups.ix_ehr_lookups_labwork_services_container; +-- Dropping ix_ehr_lookups_parentagetypes_container [container] because it overlaps with uq_parentagetypes [container, label] +DROP INDEX ehr_lookups.ix_ehr_lookups_parentagetypes_container; diff --git a/ehr/resources/web/ehr/data/StoreCollection.js b/ehr/resources/web/ehr/data/StoreCollection.js index 4efa982b0..45c893b20 100644 --- a/ehr/resources/web/ehr/data/StoreCollection.js +++ b/ehr/resources/web/ehr/data/StoreCollection.js @@ -12,6 +12,7 @@ Ext4.define('EHR.data.StoreCollection', { serverStores: null, hasLoaded: false, //will be set true after initial load clientDataChangeBuffer: 150, + validationRequestsInFlight: 0, ignoredClientEvents: {}, constructor: function(){ @@ -20,7 +21,7 @@ Ext4.define('EHR.data.StoreCollection', { this.serverStores = Ext4.create('Ext.util.MixedCollection', false, this.getKey); this.callParent(arguments); - this.addEvents('commitcomplete', 'commitexception', 'validation', 'initialload', 'load', 'clientdatachanged', 'serverdatachanged'); + this.addEvents('commitcomplete', 'commitexception', 'beforevalidation', 'validationstart', 'validation', 'validationcomplete', 'initialload', 'load', 'clientdatachanged', 'serverdatachanged'); this.on('clientdatachanged', this.onClientDataChanged, this, {buffer: this.clientDataChangeBuffer}); }, @@ -218,24 +219,48 @@ Ext4.define('EHR.data.StoreCollection', { } else { - //this really isnt the right event to fire, but it will force a recalulation of buttons on the panel + //this really isn't the right event to fire, but it will force a recalculation of buttons on the panel this.fireEvent('validation', this); } }, validateAll: function(){ + if(this.fireEvent('beforevalidation', this)===false) + return; this.serverStores.each(function(serverStore){ serverStore.validateRecords(serverStore.getRange(), true); }, this); }, validateRecords: function(recordMap){ + if(this.fireEvent('beforevalidation', this)===false) + return; for (var serverStoreId in recordMap){ var serverStore = this.serverStores.get(serverStoreId); serverStore.validateRecords(Ext4.Object.getValues(recordMap[serverStoreId]), true); } }, + onValidationRequestStart: function(){ + this.validationRequestsInFlight++; + + if (this.validationRequestsInFlight === 1){ + this.fireEvent('validationstart', this); + } + }, + + onValidationRequestComplete: function(){ + if (!this.validationRequestsInFlight){ + return; + } + + this.validationRequestsInFlight--; + + if (this.validationRequestsInFlight === 0){ + this.fireEvent('validationcomplete', this); + } + }, + serverToClientDataMap: null, getServerToClientDataMap: function(){ @@ -472,11 +497,31 @@ Ext4.define('EHR.data.StoreCollection', { if (EHR.debug) console.log(commands); + var success = this.getOnCommitSuccess(recordsArr, validateOnly, retainErrors); + var failure = this.getOnCommitFailure(recordsArr, validateOnly); var cfg = { url : LABKEY.ActionURL.buildURL('query', 'saveRows', this.containerPath), method : 'POST', - success: this.getOnCommitSuccess(recordsArr, validateOnly, retainErrors), - failure: this.getOnCommitFailure(recordsArr, validateOnly), + success: function(response, options){ + try { + success.call(this, response, options); + } + finally { + if (validateOnly){ + this.onValidationRequestComplete(); + } + } + }, + failure: function(response, options){ + try { + failure.call(this, response, options); + } + finally { + if (validateOnly){ + this.onValidationRequestComplete(); + } + } + }, scope: this, timeout: 5000000, //a little extreme? transacted: true, @@ -498,9 +543,20 @@ Ext4.define('EHR.data.StoreCollection', { if (validateOnly){ cfg.jsonData.validateOnly = true; cfg.jsonData.extraContext.isValidateOnly = true; + this.onValidationRequestStart(); + } + + var request; + try { + request = LABKEY.Ajax.request(cfg); } + catch (e){ + if (validateOnly){ + this.onValidationRequestComplete(); + } - var request = LABKEY.Ajax.request(cfg); + throw e; + } Ext4.Array.forEach(recordsArr, function(command){ Ext4.Array.forEach(command, function(rec){ @@ -893,4 +949,4 @@ Ext4.define('EHR.data.StoreCollection', { s.checkForServerErrorChanges(); }, this); } -}); \ No newline at end of file +}); diff --git a/ehr/resources/web/ehr/panel/DataEntryErrorPanel.js b/ehr/resources/web/ehr/panel/DataEntryErrorPanel.js index ba21677d1..c7eff1426 100644 --- a/ehr/resources/web/ehr/panel/DataEntryErrorPanel.js +++ b/ehr/resources/web/ehr/panel/DataEntryErrorPanel.js @@ -19,6 +19,7 @@ Ext4.define('EHR.panel.DataEntryErrorPanel', { this.callParent(arguments); this.mon(this.storeCollection, 'validation', this.updateErrorMessages, this, {buffer: 1000}); + this.mon(this.storeCollection, 'validationcomplete', this.updateErrorMessages, this, {buffer: 50}); this.mon(this.storeCollection, 'commitcomplete', this.updateErrorMessages, this, {buffer: 200}); this.mon(this.storeCollection, 'commitexception', this.updateErrorMessages, this, {buffer: 200}); }, diff --git a/ehr/resources/web/ehr/panel/DataEntryPanel.js b/ehr/resources/web/ehr/panel/DataEntryPanel.js index ea403c084..1c7ff43ae 100644 --- a/ehr/resources/web/ehr/panel/DataEntryPanel.js +++ b/ehr/resources/web/ehr/panel/DataEntryPanel.js @@ -12,6 +12,7 @@ Ext4.define('EHR.panel.DataEntryPanel', { storeCollection: null, hideErrorPanel: false, useSectionBorder: true, + validationInProgress: false, layout: 'anchor', border: false, @@ -33,7 +34,10 @@ Ext4.define('EHR.panel.DataEntryPanel', { this.storeCollection.on('initialload', this.onStoreCollectionInitialLoad, this); this.storeCollection.on('commitcomplete', this.onStoreCollectionCommitComplete, this); this.storeCollection.on('validation', this.onStoreCollectionValidation, this); + this.storeCollection.on('validationstart', this.onValidationStart, this); + this.storeCollection.on('validationcomplete', this.onValidationComplete, this); this.storeCollection.on('beforecommit', this.onStoreCollectionBeforeCommit, this); + this.storeCollection.on('beforevalidation', this.onBeforeValidation, this); this.storeCollection.on('commitexception', this.onStoreCollectionCommitException, this); //this.storeCollection.on('serverdatachanged', this.onStoreCollectionServerDataChanged, this); @@ -83,6 +87,56 @@ Ext4.define('EHR.panel.DataEntryPanel', { } }, + onBeforeValidation: function(sc){ + function processItem(item) { + if(item.disableOn) { + item.setDisabled(true); + if (item.setTooltip) + item.setTooltip('Disabled waiting on validation. Select "More Actions" -> "Re-Validate" if this is not clearing.'); + } + + if (item.menu) { + item.menu.items.each(function (menuItem) { + processItem(menuItem); + }, this); + } + } + + var ehrContext = LABKEY.getModuleContext('ehr'); + if (ehrContext && !ehrContext.isSubmitEnabledOnValidation) { + var btns = this.getToolbarItems(); + if (btns) { + Ext4.Array.forEach(btns, function (toolbar) { + toolbar.items.each(function (item) { + processItem(item); + }, this); + }, this); + } + } + }, + + onValidationStart: function(){ + // Suppress the indicator during initial form load/background reconciliation. + if (!this.hasStoreCollectionLoaded || !this.storeCollection || !this.storeCollection.hasLoaded){ + return; + } + + this.validationInProgress = true; + this.setValidationIndicatorVisible(true); + }, + + onValidationComplete: function(){ + this.validationInProgress = false; + this.setValidationIndicatorVisible(false); + + var errorPanel = this.getErrorPanel(); + if (errorPanel){ + errorPanel.updateErrorMessages(); + } + + this.onStoreCollectionValidation(this.storeCollection); + }, + onStoreCollectionValidation: function(sc){ if (!this.hasStoreCollectionLoaded){ return; @@ -90,6 +144,10 @@ Ext4.define('EHR.panel.DataEntryPanel', { this.updateDirtyStateMessage(); + if (this.storeCollection && this.storeCollection.validationRequestsInFlight > 0){ + return; + } + var maxSeverity = sc.getMaxErrorSeverity(); if(EHR.debug && maxSeverity) @@ -276,6 +334,19 @@ Ext4.define('EHR.panel.DataEntryPanel', { }, items: this.getItemConfig(), dockedItems: [{ + xtype: 'toolbar', + dock: 'bottom', + itemId: 'validationIndicator', + hidden: true, + border: false, + plain: true, + style: 'background-color: transparent; padding: 12px 0 0 0;', + items: [{ + xtype: 'container', + html: ' Validating...', + style: 'font: bold 13px tahoma,arial,verdana,sans-serif; line-height: 16px; color: #C33;' + }] + },{ xtype: 'toolbar', dock: 'bottom', ui: 'footer', @@ -509,6 +580,29 @@ Ext4.define('EHR.panel.DataEntryPanel', { return this.dirtyStateArea; }, + getErrorPanel: function(){ + if (!this.errorPanel || this.errorPanel.isDestroyed){ + this.errorPanel = this.down('#errorPanel'); + } + + return this.errorPanel; + }, + + getValidationIndicator: function(){ + if (!this.validationIndicator || this.validationIndicator.isDestroyed){ + this.validationIndicator = this.down('#validationIndicator'); + } + + return this.validationIndicator; + }, + + setValidationIndicatorVisible: function(visible){ + var indicator = this.getValidationIndicator(); + if (indicator){ + indicator.setVisible(visible); + } + }, + getButtons: function(){ var buttons = [{ xtype: 'container', diff --git a/ehr/src/org/labkey/ehr/EHRManager.java b/ehr/src/org/labkey/ehr/EHRManager.java index 085213cd3..63b92bb2c 100644 --- a/ehr/src/org/labkey/ehr/EHRManager.java +++ b/ehr/src/org/labkey/ehr/EHRManager.java @@ -136,6 +136,8 @@ public class EHRManager public static final String EXPERIMENTAL_REACT_PARTICIPANT_REPORTS = "ehrReactParticipantReports"; + public static final String EXPERIMENTAL_SUBMIT_ENABLED_ON_VALIDATION = "ehrSubmitEnabledDuringValidation"; + // Column name constants to reduce hardcoding private static final class ColumnNames { diff --git a/ehr/src/org/labkey/ehr/EHRModule.java b/ehr/src/org/labkey/ehr/EHRModule.java index acb836e9e..953be0a11 100644 --- a/ehr/src/org/labkey/ehr/EHRModule.java +++ b/ehr/src/org/labkey/ehr/EHRModule.java @@ -135,7 +135,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 26.002; + return 26.003; } @Override @@ -296,6 +296,11 @@ public void moduleStartupComplete(ServletContext servletContext) "Use React EHR participant history", "Links on animal Ids will go to the new React participant history.", false); + + OptionalFeatureService.get().addExperimentalFeatureFlag(EHRManager.EXPERIMENTAL_SUBMIT_ENABLED_ON_VALIDATION, + "Enable 'Submit' buttons during validation", + "User can submit form while validation is in progress", + false); } @Override @@ -337,6 +342,9 @@ public JSONObject getPageContextJson(ContainerUser context) // Expose the experimental React participant reports flag to client-side JavaScript ret.put("isReactAnimalHistoryEnabled", AppProps.getInstance().isOptionalFeatureEnabled(EHRManager.EXPERIMENTAL_REACT_PARTICIPANT_REPORTS)); + // Expose the experimental 'submit enabled on validation' flag to client-side JavaScript + ret.put("isSubmitEnabledOnValidation", AppProps.getInstance().isOptionalFeatureEnabled(EHRManager.EXPERIMENTAL_SUBMIT_ENABLED_ON_VALIDATION)); + if (map.containsKey(EHRManager.EHRStudyContainerPropName) && map.get(EHRManager.EHRStudyContainerPropName) != null) { User u = context.getUser(); diff --git a/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java b/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java index b46909ce3..c3bcfe483 100644 --- a/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java +++ b/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java @@ -1796,75 +1796,47 @@ private void appendAgeAtTimeCol(UserSchema ehrSchema, AbstractTableInfo ds, fina @Override public TableInfo getLookupTableInfo() { - String name = queryName + "_ageAtTime"; + String lookupQueryName = queryName + "_ageAtTime"; + String dateSql = "c." + FieldKey.fromString(dateColName).toSQLString(); + String pkSql = "c." + pkCol.getFieldKey().toSQLString(); UserSchema targetSchema = ds.getUserSchema().getDefaultSchema().getUserSchema(targetSchemaName); - QueryDefinition qd = QueryService.get().createQueryDef(u, targetSchemaContainer, targetSchema, name); + QueryDefinition qd = QueryService.get().createQueryDef(u, targetSchemaContainer, targetSchema, lookupQueryName); + // Compute the "as-of" date once: clamp to lastDayAtCenter when the record is after the animal left the center. + // All downstream age expressions reference x.effDate, so the clamp lives in one place. //NOTE: do not need to account for QCstate b/c study.demographics only allows 1 row per subject - qd.setSql("SELECT\n" + - "c." + pkCol.getFieldKey().toSQLString() + ",\n" + - "\n" + - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " ROUND(CONVERT(age_in_months(d.birth, d.lastDayAtCenter), DOUBLE) / 12, 1)\n" + - "ELSE\n" + - " ROUND(CONVERT(age_in_months(d.birth, CAST(c." + dateColName + " as DATE)), DOUBLE) / 12, 1)\n" + - "END AS float) as AgeAtTime,\n" + - "\n" + - - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " ROUND(CONVERT(timestampdiff('SQL_TSI_DAY', d.birth, d.lastDayAtCenter), DOUBLE) / 365.25, 2)\n" + - "ELSE\n" + - " ROUND(CONVERT(timestampdiff('SQL_TSI_DAY', d.birth, CAST(c." + dateColName + " as DATE)), DOUBLE) / 365.25, 2)\n" + - "END AS float) as AgeAtTimeYears,\n" + - "\n" + - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " floor(age(d.birth, d.lastDayAtCenter))\n" + - "ELSE\n" + - " floor(age(d.birth, CAST(c." + dateColName + " as DATE)))\n" + - "END AS float) as AgeAtTimeYearsRounded,\n" + - "\n" + - //Added 'Age at time Days' by kollil on 02/15/2019 - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " CONVERT(TIMESTAMPDIFF('SQL_TSI_DAY',d.birth, d.lastDayAtCenter), INTEGER)\n" + - "ELSE\n" + - " CONVERT(TIMESTAMPDIFF('SQL_TSI_DAY',d.birth, CAST(c." + dateColName + " AS DATE)), INTEGER)\n" + - "END AS float) as AgeAtTimeDays,\n" + - "\n" + - // - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " CONVERT(age_in_months(d.birth, d.lastDayAtCenter), INTEGER)\n" + - "ELSE\n" + - " CONVERT(age_in_months(d.birth, CAST(c." + dateColName + " AS DATE)), INTEGER)\n" + - "END AS float) as AgeAtTimeMonths,\n" + - //NOTE: written as subselect so we ensure a single row returned in case data in ehr_lookups.ageclass has rows that allow dupes - "(SELECT ac.ageclass FROM ehr_lookups.ageclass ac\n" + - " WHERE " + - " (CONVERT(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), DOUBLE) / 12) >= ac.\"min\" AND\n" + - " ((CONVERT(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), DOUBLE) / 12) < ac.\"max\" OR ac.\"max\" is null) AND\n" + - " d.species = ac.species AND\n" + - " (d.gender = ac.gender OR ac.gender IS NULL)\n" + - ") AS AgeClassAtTime \n" + - "FROM \"" + schemaName + "\".\"" + queryName + "\" c " + - "LEFT JOIN \"" + ehrPath + "\".study.demographics d ON (d.Id = c." + idCol.getFieldKey().toSQLString() + ")" + String pkAlias = pkCol.getFieldKey().toSQLString(); + qd.setSql( + "SELECT\n" + + " x." + pkAlias + ",\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE ROUND(CONVERT(age_in_months(x.birth, x.effDate), DOUBLE) / 12, 1) END AS float) AS AgeAtTime,\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE ROUND(CONVERT(age_in_days(x.birth, x.effDate), DOUBLE) / 365.25, 2) END AS float) AS AgeAtTimeYears,\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE floor(age(x.birth, x.effDate)) END AS float) AS AgeAtTimeYearsRounded,\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE age_in_days(x.birth, x.effDate) END AS float) AS AgeAtTimeDays,\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE CONVERT(age_in_months(x.birth, x.effDate), INTEGER) END AS float) AS AgeAtTimeMonths,\n" + + //NOTE: written as subselect so we ensure a single row returned in case data in ehr_lookups.ageclass has rows that allow dupes + " (SELECT ac.ageclass FROM ehr_lookups.ageclass ac\n" + + " WHERE (CONVERT(age_in_months(x.birth, x.effDate), DOUBLE) / 12) >= ac.\"min\"\n" + + " AND ((CONVERT(age_in_months(x.birth, x.effDate), DOUBLE) / 12) < ac.\"max\" OR ac.\"max\" IS NULL)\n" + + " AND x.species = ac.species\n" + + " AND (x.gender = ac.gender OR ac.gender IS NULL)\n" + + " ) AS AgeClassAtTime\n" + + "FROM (\n" + + " SELECT\n" + + " " + pkSql + " AS " + pkAlias + ",\n" + + " d.birth AS birth,\n" + + " d.species AS species,\n" + + " d.gender AS gender,\n" + + " CASE WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < " + dateSql + ")\n" + + " THEN d.lastDayAtCenter\n" + + " ELSE CAST(" + dateSql + " AS DATE) END AS effDate\n" + + " FROM \"" + schemaName + "\".\"" + queryName + "\" c\n" + + " LEFT JOIN \"" + ehrPath + "\".study.demographics d ON (d.Id = c." + idCol.getFieldKey().toSQLString() + ")\n" + + ") x" ); qd.setIsTemporary(true); @@ -1877,12 +1849,17 @@ public TableInfo getLookupTableInfo() { _log.warn(e.getMessage(), e); } + return null; } if (ti != null) { - ((BaseColumnInfo)ti.getColumn(pkCol.getName())).setHidden(true); - ((BaseColumnInfo)ti.getColumn(pkCol.getName())).setKeyField(true); + ColumnInfo pk = ti.getColumn(pkCol.getName()); + if (pk instanceof BaseColumnInfo basePk) + { + basePk.setHidden(true); + basePk.setKeyField(true); + } } else { @@ -1891,7 +1868,7 @@ public TableInfo getLookupTableInfo() if (demographics != null) { _log.warn("Demographics table columns: "); - _log.warn(targetSchema.getTable("demographics").getColumnNameSet()); + _log.warn(demographics.getColumnNameSet()); } } diff --git a/ehr_billing/resources/schemas/dbscripts/postgresql/ehr_billing-26.000-26.001.sql b/ehr_billing/resources/schemas/dbscripts/postgresql/ehr_billing-26.000-26.001.sql new file mode 100644 index 000000000..21c37d565 --- /dev/null +++ b/ehr_billing/resources/schemas/dbscripts/postgresql/ehr_billing-26.000-26.001.sql @@ -0,0 +1,6 @@ +-- Dropping unique_invoice_num [invoiceNumber] because it overlaps with pk_ehr_billing_invoice_invnum [invoiceNumber] +ALTER TABLE ehr_billing.invoice DROP CONSTRAINT unique_invoice_num; +-- Dropping ix_ehr_billing_invoice_invoicenumber [invoiceNumber] because it overlaps with pk_ehr_billing_invoice_invnum [invoiceNumber] +DROP INDEX ehr_billing.ix_ehr_billing_invoice_invoicenumber; +-- Dropping uq_ehr_billing_invoice_invoicenumber [invoiceNumber] because it overlaps with pk_ehr_billing_invoice_invnum [invoiceNumber] +ALTER TABLE ehr_billing.invoice DROP CONSTRAINT uq_ehr_billing_invoice_invoicenumber; diff --git a/ehr_billing/src/org/labkey/ehr_billing/EHR_BillingModule.java b/ehr_billing/src/org/labkey/ehr_billing/EHR_BillingModule.java index 2599c5c76..f1c2d6f35 100644 --- a/ehr_billing/src/org/labkey/ehr_billing/EHR_BillingModule.java +++ b/ehr_billing/src/org/labkey/ehr_billing/EHR_BillingModule.java @@ -63,7 +63,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 26.000; + return 26.001; } @Override