diff --git a/client/src/i18n/en/stock.json b/client/src/i18n/en/stock.json index 962429e32b..6d3472c8ff 100644 --- a/client/src/i18n/en/stock.json +++ b/client/src/i18n/en/stock.json @@ -101,7 +101,7 @@ "ITEMS" : "Items", "LEGEND" : "Legend", "LIFETIME" : "Lifetime", - "LOT_LIFETIME" : "Lifetime By Lot", + "LOT_LIFETIME" : "Lifetime by Usage", "CURRENT_QUANTITY" : "Current Quantity", "RISK" : "Risk", "RISK_QUANTITY" : "Risk Quantity", @@ -192,6 +192,7 @@ "REQUIRES_PO" : "Requires a purchase order", "RESPONSIBLE" : "Stock Manager", "RISK_OF_EXPIRATION" : "Risk of expiration", + "RISK_OF_STOCK_OUT" : "Risk of stock out", "ROWS" : "Rows", "SEARCH" : "Search", "SECURITY" : "Security Stock", diff --git a/client/src/i18n/fr/stock.json b/client/src/i18n/fr/stock.json index 631975dea9..e39d7d6d0e 100644 --- a/client/src/i18n/fr/stock.json +++ b/client/src/i18n/fr/stock.json @@ -192,6 +192,7 @@ "REQUIRES_PO" : "Nécessitant une commande d'achat", "RESPONSIBLE" : "Gestionnaire du stock", "RISK_OF_EXPIRATION" : "Risque de péremption", + "RISK_OF_STOCK_OUT" : "Risque de rupture", "ROWS" : "Lignes", "SEARCH" : "Chercher", "SECURITY" : "Stock de securité", diff --git a/client/src/js/services/LotService.js b/client/src/js/services/LotService.js index aca888c3b5..d532987688 100644 --- a/client/src/js/services/LotService.js +++ b/client/src/js/services/LotService.js @@ -45,12 +45,12 @@ function LotService(Api, $http, util) { * NOTE: Once a case is found to be true, all following cases are ignored. * 1. If the stock is exhausted, warn about that. * (Recall that the user can choose to display exhausted lots in Lots Registry.) - * 2. If the Lot is expired, warn about that. - * 3. If the Lot is near expiration, warn about that. + * 2. If the Lot is near expiration, warn about that. * NOTE that this assumes the CMM and that stock exits all come from this * lot exclusively. But the CMM is based on not only on this lot, but on * an aggregate all lots of this inventory item, so there is no guarantee * that this will be correct. + * 3. If the Lot is expired, warn about that. * 4. If a Lot is at risk of running out, warn about that. Again this is * based on the aggregate CMM which may not work out exactly for this * Lot in practice. @@ -58,10 +58,11 @@ function LotService(Api, $http, util) { * Based on this logic, only one of the warning flags should be set to true. */ lots.computeLotWarningFlags = (lot) => { - lots.exhausted = false; - lots.expired = false; - lots.near_expiration = false; - lots.at_risk = false; + lot.exhausted = false; + lot.expired = false; + lot.near_expiration = false; + lot.at_risk = false; + if (lot.quantity <= 0) { lot.exhausted = true; } else if (lot.lifetime < 0) { @@ -75,6 +76,7 @@ function LotService(Api, $http, util) { lot.at_risk = true; } + return lot; }; return lots; diff --git a/client/src/modules/stock/lots/registry.js b/client/src/modules/stock/lots/registry.js index d673fc62ef..0cd0b9cdc0 100644 --- a/client/src/modules/stock/lots/registry.js +++ b/client/src/modules/stock/lots/registry.js @@ -22,6 +22,7 @@ function StockLotsController( // grouping box vm.groupingBox = LotsRegistry.groupingBox; + // barcode scanner vm.openBarcodeScanner = openBarcodeScanner; @@ -145,23 +146,46 @@ function StockLotsController( .then((lots) => { const current = new Date(); + const totals = { + expired : 0, + 'at-risk-of-expiring' : 0, + 'at-risk' : 0, + 'out-of-stock' : 0, + }; + lots.forEach((lot) => { const delay = moment(new Date(lot.expiration_date)).diff(current); lot.delay_expiration = moment.duration(delay).humanize(true); - LotService.computeLotWarningFlags(lot); + + if (lot.expired) { + totals.expired += 1; + } + + if (lot.at_risk) { + totals['at-risk'] += 1; + } + + if (lot.near_expiration) { + totals['at-risk-of-expiring'] += 1; + } + + if (lot.exhausted) { + totals['out-of-stock'] += 1; + } + + // serialize tag names for filters + lot.tagNames = lot.tags.map(tag => tag.name).join(','); + lot.tags.forEach(addColorStyle); }); + vm.totals = totals; + lots.forEach(LotsRegistry.formatLotsWithoutExpirationDate); // FIXME(@jniles): we should do this ordering on the server via an ORDER BY lots.sort(LotsRegistry.orderByDepot); - // serialize tag names for filters - vm.gridOptions.data = lots.map(lot => { - lot.tagNames = lot.tags.map(tag => tag.name).join(','); - lot.tags.forEach(addColorStyle); - return lot; - }); + vm.gridOptions.data = lots; vm.grouping.unfoldAllGroups(); vm.gridApi.core.notifyDataChange(uiGridConstants.dataChange.COLUMN); diff --git a/client/src/modules/stock/lots/registry.service.js b/client/src/modules/stock/lots/registry.service.js index a71d5dcd81..a7c8a8fe5c 100644 --- a/client/src/modules/stock/lots/registry.service.js +++ b/client/src/modules/stock/lots/registry.service.js @@ -92,70 +92,6 @@ function LotsRegistryService(uiGridConstants, Session) { cellClass : 'text-right', headerCellFilter : 'translate', }, { - field : 'avg_consumption', - displayName : 'STOCK.CMM', - headerTooltip : 'STOCK.CMM', - cellClass : 'text-right', - headerCellFilter : 'translate', - type : 'number', - }, { - field : 'S_MONTH', - displayName : 'STOCK.MSD', - headerTooltip : 'STOCK.MSD', - cellClass : 'text-right', - headerCellFilter : 'translate', - type : 'number', - }, { - field : 'lifetime', - displayName : 'STOCK.LIFETIME', - headerTooltip : 'STOCK.LIFETIME', - headerCellFilter : 'translate', - cellClass : 'text-right', - cellTemplate : 'modules/stock/lots/templates/lifetime.cell.html', - type : 'number', - sort : { - direction : uiGridConstants.ASC, - priority : 2, - }, - }, { - field : 'S_LOT_LIFETIME', - displayName : 'STOCK.LOT_LIFETIME', - headerTooltip : 'STOCK.LOT_LIFETIME', - headerCellFilter : 'translate', - cellTemplate : 'modules/stock/lots/templates/lot_lifetime.cell.html', - cellClass : 'text-right', - type : 'number', - }, { - field : 'S_RISK', - displayName : 'STOCK.RISK', - headerTooltip : 'STOCK.RISK', - headerCellFilter : 'translate', - cellTemplate : 'modules/stock/lots/templates/risk.cell.html', - cellClass : 'text-right', - type : 'number', - sort : { - direction : uiGridConstants.DESC, - priority : 3, - }, - }, - { - field : 'IS_IN_RISK_EXPIRATION', - displayName : 'STOCK.STATUS.IS_IN_RISK_OF_EXPIRATION', - headerTooltip : 'STOCK.STATUS.IS_IN_RISK_OF_EXPIRATION', - headerCellFilter : 'translate', - cellTemplate : 'modules/stock/lots/templates/in_risk_of_expiration.html', - cellClass : 'text-right', - type : 'number', - }, - { - field : 'S_RISK_QUANTITY', - displayName : 'STOCK.RISK_QUANTITY', - headerTooltip : 'STOCK.RISK_QUANTITY', - headerCellFilter : 'translate', - cellTemplate : 'modules/stock/lots/templates/risk_quantity.cell.html', - type : 'number', - }, - { field : 'tagNames', displayName : 'TAG.LABEL', headerTooltip : 'TAG.LABEL', @@ -176,18 +112,24 @@ function LotsRegistryService(uiGridConstants, Session) { - STOCK.EXPIRATION: + STOCK.EXPIRED: {{grid.appScope.totals.expired}} + + + + + STOCK.RISK_OF_EXPIRATION: {{grid.appScope.totals['at-risk-of-expiring']}} - STOCK.RISK_OF_EXPIRATION: + STOCK.RISK_OF_STOCK_OUT: {{grid.appScope.totals['at-risk']}} - STOCK.STATUS.STOCK_OUT: + STOCK.STATUS.STOCK_OUT: {{grid.appScope.totals['out-of-stock']}} + `; @@ -205,6 +147,8 @@ function LotsRegistryService(uiGridConstants, Session) { delete lot.expiration_date; delete lot.lifetime; delete lot.S_LOT_LIFETIME; + delete lot.S_RISK; + delete lot.S_RISK_QUANTITY; } }; diff --git a/client/src/modules/stock/lots/templates/in_risk_of_expiration.html b/client/src/modules/stock/lots/templates/in_risk_of_expiration.html deleted file mode 100644 index c5eba2beb8..0000000000 --- a/client/src/modules/stock/lots/templates/in_risk_of_expiration.html +++ /dev/null @@ -1,4 +0,0 @@ -
- FORM.LABELS.YES - FORM.LABELS.NO -
diff --git a/client/src/modules/stock/lots/templates/lifetime.cell.html b/client/src/modules/stock/lots/templates/lifetime.cell.html index 30381a888e..c097a046b5 100644 --- a/client/src/modules/stock/lots/templates/lifetime.cell.html +++ b/client/src/modules/stock/lots/templates/lifetime.cell.html @@ -1,5 +1,5 @@
- - {{ row.entity.lifetime }} FORM.LABELS.MONTH + + {{ row.entity.lifetime }} FORM.LABELS.DAYS
diff --git a/client/src/modules/stock/lots/templates/lot_lifetime.cell.html b/client/src/modules/stock/lots/templates/lot_lifetime.cell.html index cc63cabd6f..6e4fe2891d 100644 --- a/client/src/modules/stock/lots/templates/lot_lifetime.cell.html +++ b/client/src/modules/stock/lots/templates/lot_lifetime.cell.html @@ -1,5 +1,5 @@
- - {{ row.entity.S_LOT_LIFETIME }} FORM.LABELS.MONTH + + {{ row.entity.lifetime_lot}} FORM.LABELS.DAYS
diff --git a/client/src/modules/stock/lots/templates/risk.cell.html b/client/src/modules/stock/lots/templates/risk.cell.html index fd1203f3b8..3070c14123 100644 --- a/client/src/modules/stock/lots/templates/risk.cell.html +++ b/client/src/modules/stock/lots/templates/risk.cell.html @@ -1,5 +1,5 @@ -
- - {{ row.entity.S_RISK }} FORM.LABELS.MONTH +
+ + {{ row.entity.S_RISK }} FORM.LABELS.DAYS
diff --git a/client/src/modules/stock/lots/templates/risk_quantity.cell.html b/client/src/modules/stock/lots/templates/risk_quantity.cell.html index c91fef5f39..e4b4b5e40b 100644 --- a/client/src/modules/stock/lots/templates/risk_quantity.cell.html +++ b/client/src/modules/stock/lots/templates/risk_quantity.cell.html @@ -1,3 +1,3 @@ -
+
{{ row.entity.S_RISK_QUANTITY }} -
\ No newline at end of file +
diff --git a/client/src/modules/stock/lots/templates/row.expired.html b/client/src/modules/stock/lots/templates/row.expired.html index 7b230cc4f0..4f4f9b848b 100644 --- a/client/src/modules/stock/lots/templates/row.expired.html +++ b/client/src/modules/stock/lots/templates/row.expired.html @@ -4,8 +4,12 @@ ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.uid" ui-grid-one-bind-id-grid="rowRenderIndex + '-' + col.uid + '-cell'" class="ui-grid-cell" - ng-class="{ 'ui-grid-row-header-cell': col.isRowHeader, 'at-risk-of-expiring': row.entity.near_expiration, - 'expired': row.entity.expired, 'at-risk': row.entity.at_risk, 'out-of-stock': row.entity.exhausted, + ng-class="{ + 'ui-grid-row-header-cell': col.isRowHeader, + 'at-risk-of-expiring': row.entity.near_expiration, + 'expired': row.entity.expired, + 'at-risk': row.entity.at_risk, + 'out-of-stock': row.entity.exhausted, }" data-vals="{{col.isRowHeader}}" role="{{col.isRowHeader ? 'rowheader' : 'gridcell'}}" diff --git a/package.json b/package.json index f70d0151bc..d74925e1c0 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "angular-translate-loader-static-files": "^2.18.4", "angular-translate-loader-url": "^2.18.4", "angular-ui-bootstrap": "^2.5.6", - "angular-ui-grid": "^4.9.0", + "angular-ui-grid": "^4.10.0", "body-parser": "^1.18.3", "bootstrap": "^3.3.0", "chart.js": "^2.9.4", @@ -106,7 +106,7 @@ "cross-env": "^7.0.3", "csvtojson": "^2.0.8", "debug": "^4.3.1", - "delay": "^4.4.1", + "delay": "^5.0.0", "dotenv": "^8.0.0", "excel4node": "^1.7.0", "express": "^4.16.4", @@ -167,7 +167,7 @@ "gulp-template": "^5.0.0", "gulp-typescript": "^5.0.0", "gulp-uglify": "^3.0.1", - "karma": "^6.0.3", + "karma": "^6.0.4", "karma-chai": "^0.1.0", "karma-chai-dom": "^1.1.0", "karma-chai-spies": "^0.1.4", diff --git a/server/controllers/stock/core.js b/server/controllers/stock/core.js index 27712092d4..db1a65477c 100644 --- a/server/controllers/stock/core.js +++ b/server/controllers/stock/core.js @@ -231,7 +231,7 @@ async function getLotsDepot(depotUuid, params, finalClause) { SUM(m.quantity) AS mvt_quantity, d.text AS depot_text, l.unit_cost, l.expiration_date, d.min_months_security_stock, - ROUND(DATEDIFF(l.expiration_date, CURRENT_DATE()) / 30.5) AS lifetime, + DATEDIFF(l.expiration_date, CURRENT_DATE()) AS lifetime, BUID(l.inventory_uuid) AS inventory_uuid, BUID(l.origin_uuid) AS origin_uuid, i.code, i.text, BUID(m.depot_uuid) AS depot_uuid, m.date AS entry_date, i.avg_consumption, i.purchase_interval, i.delay, @@ -266,23 +266,22 @@ async function getLotsDepot(depotUuid, params, finalClause) { params.average_consumption_algo, ); - // FIXME(@jniles) - this step seems to mostly just change the ordering of lots. Can we combine - // it with the getBulkInventoryCMM? + // add lot indicators to the inventory list. let inventoriesWithLotsProcessed = computeLotIndicators(inventoriesWithManagementData); if (_status) { - inventoriesWithLotsProcessed = inventoriesWithLotsProcessed.filter(row => row.status === _status); + inventoriesWithLotsProcessed = inventoriesWithLotsProcessed.filter(lot => lot.status === _status); } // Since the status of a product risking expiry is only defined // after the comparison with the CMM, reason why the filtering // is not carried out with an SQL request if (parseInt(params.is_expiry_risk, 10) === 1) { - inventoriesWithLotsProcessed = inventoriesWithLotsProcessed.filter(item => (item.S_RISK < 0 && item.lifetime > 0)); + inventoriesWithLotsProcessed = inventoriesWithLotsProcessed.filter(lot => lot.flags.near_expiration); } if (parseInt(params.is_expiry_risk, 10) === 0) { - inventoriesWithLotsProcessed = inventoriesWithLotsProcessed.filter(item => (item.S_RISK >= 0 && item.lifetime > 0)); + inventoriesWithLotsProcessed = inventoriesWithLotsProcessed.filter(lot => !lot.flags.near_expiration); } return inventoriesWithLotsProcessed; @@ -647,7 +646,7 @@ async function getInventoryQuantityAndConsumption(params) { SUM(m.quantity * IF(m.is_exit = 1, -1, 1)) AS quantity, d.text AS depot_text, d.min_months_security_stock, l.unit_cost, l.expiration_date, - ROUND(DATEDIFF(l.expiration_date, CURRENT_DATE()) / 30.5) AS lifetime, + DATEDIFF(l.expiration_date, CURRENT_DATE()) AS lifetime, BUID(l.inventory_uuid) AS inventory_uuid, BUID(l.origin_uuid) AS origin_uuid, l.entry_date, BUID(i.uuid) AS inventory_uuid, i.code, i.text, BUID(m.depot_uuid) AS depot_uuid, i.avg_consumption, i.purchase_interval, i.delay, MAX(m.created_at) AS last_movement_date, @@ -693,16 +692,35 @@ async function getInventoryQuantityAndConsumption(params) { * process multiple stock lots * * @description + * Computes the indicators on stock lots. Sets the following flags: + * 1) expired - if the expiration date is in the past + * 2) exhausted - if the quantity is 0. + * 3) near_expiration - if the expiration date is sooner than the lot's stock out date + * 4) at_risk - if the lot is part of an _inventory_ that is at risk of running out. + * + * Further, we add the following properties: + * 1) lot_lifetime - the number of days left before the stock runs out. + * 2) max_stock_date - the last day stock will be usable. + * 3) min_stock_date - the first day the stock will be consumed + * 4) usable_quantity_remaining - the total quantity that will be usable of the lot. + * + * Note that these indicators are only tracked if we are tracking_consumption + * or tracking_expiration. + * + * The algorithm for calculation at risk of expiry and at risk of stock out is + * FIFO by expiration. Basically, we assume that lots will be used in order of + * their expiration dates. */ function computeLotIndicators(inventories) { const flattenLots = []; - const inventoryByDepots = _.groupBy(inventories, 'depot_uuid'); - _.map(inventoryByDepots, (depotInventories) => { + const inventoryByDepots = _.groupBy(inventories, 'depot_uuid'); + _.forEach(inventoryByDepots, (depotInventories) => { const inventoryLots = _.groupBy(depotInventories, 'inventory_uuid'); - _.map(inventoryLots, (lots) => { + _.forEach(inventoryLots, (lots) => { + // if we don't have the default CMM (avg_consumption) use the // defined or computed CMM for each lots const cmm = _.max(lots.map(lot => lot.avg_consumption)); @@ -711,36 +729,84 @@ function computeLotIndicators(inventories) { // assuming the lot with lowest quantity is consumed first let orderedInventoryLots = _.orderBy(lots, 'quantity', 'asc'); - // order lots by ascending lifetime has a hight priority than quantity + // order lots by ascending lifetime has a higher priority than quantity orderedInventoryLots = _.orderBy(orderedInventoryLots, 'lifetime', 'asc'); - // compute the lot coefficient - let lotLifetime = 0; - _.each(orderedInventoryLots, lot => { + // compute the lot coefficients + let runningLotLifetimes = 0; + const today = moment().endOf('day').toDate(); + + _.forEach(orderedInventoryLots, (lot) => { if (!lot.tracking_expiration) { lot.expiration_date = ''; } - if (lot.tracking_consumption) { + lot.exhausted = lot.quantity === 0; + lot.expired = !lot.exhausted && (lot.expiration_date < today); + + // algorithm for tracking the stock consumption by day + if (lot.tracking_consumption && !lot.exhausted && !lot.expired) { // apply the same CMM to all lots and update monthly consumption lot.avg_consumption = cmm; lot.S_MONTH = cmm ? Math.floor(lot.quantity / cmm) : lot.quantity; - const zeroMSD = Math.round(lot.S_MONTH) === 0; + // get daily average consumption + const consumptionPerDay = lot.avg_consumption / 30.5; + + // check if the lot will expire before being used up + const numDaysOfStockBeforeConsumed = runningLotLifetimes + Math.ceil(lot.quantity / consumptionPerDay); + const stockOutDate = moment(new Date()).add(numDaysOfStockBeforeConsumed, 'days').toDate(); + + lot.near_expiration = lot.expiration_date <= stockOutDate; + + // here, we are attempting to calculate if this quantity will be at risk of expiry + // given that all inventory is consummed in order of their expiration dates. + + // Get the real quantity of usable stock remaining + // If the lot will expire before being used, use the expiration date as the max date. + // If the lot will be consumed by CMM before expiry, use the CMM date as the max date. + // We've already calculated this above - it is the near_expiration variable + const maxStockDate = lot.near_expiration ? lot.expiration_date : stockOutDate; + const numDaysOfStockLeft = moment(maxStockDate).diff(new Date(), 'day'); + + // calculate finally the remaining days of stock, assuming we use this stock in order + // maybe we will not ever get a chance to use it ... then it is 0. + lot.lifetime_lot = Math.max(0, numDaysOfStockLeft - runningLotLifetimes); + lot.min_stock_date = moment(new Date()).add(runningLotLifetimes).toDate(); + lot.max_stock_date = maxStockDate; + + // add to the running LotLifetimes so that the next product will be used after it. + runningLotLifetimes += lot.lifetime_lot; + + // the usable quantity remaining is the minimum of the stock actually available + // and the amount able to be consumed in the time remaining + const usableStockQuantityRemaining = Math.min(lot.lifetime_lot * consumptionPerDay, lot.quantity); + lot.usable_quantity_remaining = usableStockQuantityRemaining; + + // compute the "risk" quantities and duration. This is the amount of stock in this lot that + // the enterprise will lose at the current rate of consumption. We express this in + // both quantity and in time, despite this not corresponding to a definite time period. The + // time period should be understood as a _quantity_ measurement - how long the pharmacy could + // have run if this stock wasn't expiring. + lot.S_RISK_QUANTITY = Math.round(lot.quantity - usableStockQuantityRemaining); + // if S_RISK_QUANTITY is less than a day's stock, it is likely a rounding error. Set it to 0. + if (lot.S_RISK_QUANTITY < consumptionPerDay) { lot.S_RISK_QUANTITY = 0; } + lot.S_RISK = Math.round(lot.S_RISK_QUANTITY / consumptionPerDay); + } - const numMonthsOfStockLeft = (lot.quantity / lot.CMM); // how many months of stock left - const today = new Date(); - // if we have more months of stock than the expiration date, - // then we'll need to label these are in risk of expiration - const numDaysOfStockLeft = numMonthsOfStockLeft * 30.5; - const isInRiskOfExpiration = lot.expiration_date <= moment(today).add(numDaysOfStockLeft, 'days').toDate(); - lot.IS_IN_RISK_EXPIRATION = isInRiskOfExpiration; + // if the inventory item is at risk of stock out, mark the stock lot "at risk". It may be that a single + // lot is not at risk of expiring, but the inventory is at risk of expiring + // TODO(@jniles): does this make sense? + lot.at_risk = !lot.expired && (lot.status === 'minimum_reached' || lot.status === 'security_reached'); + + // attach flags after computation for use on client + lot.flags = { + expired : lot.expired, + near_expiration : lot.near_expiration, + exhausted : lot.exhausted, + at_risk : lot.at_risk, + }; - lot.S_LOT_LIFETIME = zeroMSD || lot.lifetime < 0 ? 0 : lot.lifetime - lotLifetime; - lot.S_RISK = zeroMSD ? 0 : lot.S_LOT_LIFETIME - lot.S_MONTH; - lot.S_RISK_QUANTITY = Math.round(lot.S_RISK * lot.avg_consumption); - lotLifetime += lot.S_LOT_LIFETIME; - } flattenLots.push(lot); }); }); @@ -854,9 +920,12 @@ function getInventoryMovements(params) { }); } -function listStatus(req, res, next) { +async function listStatus(req, res, next) { const sql = `SELECT id, status_key, title_key FROM status`; - db.exec(sql).then(status => { + try { + const status = await db.exec(sql); res.status(200).json(status); - }).catch(next); + } catch (e) { + next(e); + } } diff --git a/test/integration/stock/lots.js b/test/integration/stock/lots.js index 5173c57bb5..31fd4063a3 100644 --- a/test/integration/stock/lots.js +++ b/test/integration/stock/lots.js @@ -29,7 +29,7 @@ describe('(/lots/) The lots HTTP API', () => { return agent.get('/stock/lots/depots/') .query(conditions) .then((res) => { - helpers.api.listed(res, 3); + helpers.api.listed(res, 2); }) .catch(helpers.handler); }); diff --git a/yarn.lock b/yarn.lock index a70bda6390..919f7b6bb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -598,10 +598,10 @@ angular-ui-bootstrap@^2.5.6: resolved "https://registry.yarnpkg.com/angular-ui-bootstrap/-/angular-ui-bootstrap-2.5.6.tgz#23937322ec641a6fbee16498cc32452aa199e7c5" integrity sha512-yzcHpPMLQl0232nDzm5P4iAFTFQ9dMw0QgFLuKYbDj9M0xJ62z0oudYD/Lvh1pWfRsukiytP4Xj6BHOSrSXP8A== -angular-ui-grid@^4.9.0: - version "4.9.1" - resolved "https://registry.yarnpkg.com/angular-ui-grid/-/angular-ui-grid-4.9.1.tgz#1b4df057a31318729915faad5eb42969d4298bb0" - integrity sha512-kR+B22gxqm+mhkmYgtv43uY5z1a5mk5riyL7WLygkAxjy8h0IgFi/PlkHwqA3Oit5+SpzKcTAntVg8OIwaiFog== +angular-ui-grid@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/angular-ui-grid/-/angular-ui-grid-4.10.0.tgz#5904a366c49a0a687c6a23a48b46d2b5c57d43ce" + integrity sha512-y0AtYkTf41JTmWc+hyl0bXYtUnjOWHu8JIhrxCx7kn9wdbOAkcfyYhV/MT2Cp0pS4gzUI1sjL0qqRyrTtKljnQ== dependencies: angular ">=1.4.0 1.8.x" @@ -2607,10 +2607,10 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" -delay@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/delay/-/delay-4.4.1.tgz#6e02d02946a1b6ab98b39262ced965acba2ac4d1" - integrity sha512-aL3AhqtfhOlT/3ai6sWXeqwnw63ATNpnUiN4HL7x9q+My5QtHlO3OIkasmug9LKzpheLdmUKGRKnYXYAS7FQkQ== +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== delayed-stream@~1.0.0: version "1.0.0" @@ -5488,10 +5488,10 @@ karma-ng-html2js-preprocessor@^1.0.0: resolved "https://registry.yarnpkg.com/karma-ng-html2js-preprocessor/-/karma-ng-html2js-preprocessor-1.0.0.tgz#10d8c8cfaa4136f1c8a76d91a4cbceedebec4a31" integrity sha1-ENjIz6pBNvHIp22RpMvO7evsSjE= -karma@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.0.3.tgz#a35b4371306e08e3f8e46b5753aff46eaf3eec2c" - integrity sha512-dmiLQdsNAvnbV1G6VvUK7Cl5xpwiMisZNT8MjBtOo49jKlnZSWLxQIemuLT8sGSzvx5IGgMfMQEtf/CALiUEVQ== +karma@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.0.4.tgz#5c73bb1fdecd9acf8b8d5318c48ce9c85c035f79" + integrity sha512-Yk451MSSV82wwThwissGvTpaHYhKDOzzzcguj+XYMmLF9ZN6QDnUmOexzYPeDcne6g0eWEkP1QttzX0N9lQybw== dependencies: body-parser "^1.19.0" braces "^3.0.2" @@ -5511,7 +5511,7 @@ karma@^6.0.3: qjobs "^1.2.0" range-parser "^1.2.1" rimraf "^3.0.2" - socket.io "^3.0.4" + socket.io "^3.1.0" source-map "^0.6.1" tmp "0.2.1" ua-parser-js "^0.7.23" @@ -8439,7 +8439,7 @@ socket.io-parser@~4.0.3: component-emitter "~1.3.0" debug "~4.3.1" -socket.io@^3.0.4: +socket.io@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.0.tgz#4f3accda31e95893f618090c9cb5e85d345421fb" integrity sha512-Aqg2dlRh6xSJvRYK31ksG65q4kmBOqU4g+1ukhPcoT6wNGYoIwSYPlCPuRwOO9pgLUajojGFztl6+V2opmKcww==