From 5aff7611f96e603f0825ddc2b9932348acc2246e Mon Sep 17 00:00:00 2001 From: Viktoriia Abakumova Date: Fri, 5 Jul 2024 18:00:53 +0300 Subject: [PATCH] Tactical manager scope --- src/assets/styles/analyticalView.scss | 4 + src/assets/styles/case.scss | 4 + src/assets/styles/cases.scss | 5 +- src/assets/styles/operationalView.scss | 66 ++- src/assets/styles/style.scss | 73 +++- src/common/utils.js | 8 + .../OngoingChartsManagerComponent.vue | 121 ++++++ .../PositiveOutcomeItemComponent.vue | 31 ++ src/components/ResourcesTableComponent.vue | 70 ++++ src/components/SideBarComponent.vue | 72 ++-- .../casePage/FlowDiagramComponent.vue | 156 +++---- .../casePage/ManagerViewComponent.vue | 130 ++++++ ...rationalRecommendationManagerComponent.vue | 155 +++++++ .../casePage/RecommendationComponent.vue | 128 +++--- .../casesPage/CompletedChartsComponent.vue | 20 +- .../casesPage/OngoingChartsComponent.vue | 35 +- src/pages/CasePage.vue | 24 +- src/pages/CasesPage.vue | 168 ++++---- src/pages/OngoingCasesManager.vue | 205 ++++++++++ src/pages/RecommendationsPage.vue | 379 ++++++++++++------ src/pages/ResourcesPage.vue | 99 +++++ src/router.js | 18 +- src/services/resources.service.js | 105 +++++ 23 files changed, 1659 insertions(+), 417 deletions(-) create mode 100644 src/components/OngoingChartsManagerComponent.vue create mode 100644 src/components/PositiveOutcomeItemComponent.vue create mode 100644 src/components/ResourcesTableComponent.vue create mode 100644 src/components/casePage/ManagerViewComponent.vue create mode 100644 src/components/casePage/OperationalRecommendationManagerComponent.vue create mode 100644 src/pages/OngoingCasesManager.vue create mode 100644 src/pages/ResourcesPage.vue create mode 100644 src/services/resources.service.js diff --git a/src/assets/styles/analyticalView.scss b/src/assets/styles/analyticalView.scss index 8b3ab30..3d508cd 100644 --- a/src/assets/styles/analyticalView.scss +++ b/src/assets/styles/analyticalView.scss @@ -35,6 +35,10 @@ border-radius: 10px; position: relative; + &.next_activity { + background-color: transparent; + } + .recommendation-status { position: absolute; right: 0; diff --git a/src/assets/styles/case.scss b/src/assets/styles/case.scss index a169955..1690cb6 100644 --- a/src/assets/styles/case.scss +++ b/src/assets/styles/case.scss @@ -69,6 +69,10 @@ .case-performance { padding-right: 20px; } + + .kpi { + margin-right: 1em; + } } } diff --git a/src/assets/styles/cases.scss b/src/assets/styles/cases.scss index 3b7cd93..26d650b 100644 --- a/src/assets/styles/cases.scss +++ b/src/assets/styles/cases.scss @@ -1,8 +1,7 @@ // Cases - - #cases, -#recommendations { +#recommendations, +#resource-management { padding: 20px 20px; display: flex; flex-direction: column; diff --git a/src/assets/styles/operationalView.scss b/src/assets/styles/operationalView.scss index 501466d..90ed17a 100644 --- a/src/assets/styles/operationalView.scss +++ b/src/assets/styles/operationalView.scss @@ -54,21 +54,75 @@ .recommendation { display: flex; flex-direction: row; - margin: 5px 0px; + margin: 5px 0; padding: 10px; transition: .2s; background-color: $k-blue3; border-radius: 10px; justify-content: space-between; - .column { - margin-right: 5px; + .recommendation-content { + display: flex; + justify-content: space-between; + width: 100%; + } + + .left-column { + flex: 3; + margin-right: 10px; + } + + .right-column { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-end; + } + + .text-tooltip-container { + display: flex; + align-items: center; + } + + .button-container { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-bottom: 10px; + } + + .blue-button { + background-color: $k-blue; + color: white; + border: none; + padding: 0.4em 2em; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 15px; + margin: 4px 2px; + cursor: pointer; + border-radius: 25px; + } + + &.next_activity { + background-color: transparent; } } - // .selected{ - // background-color: $k-blue4; - // } + } + + .resource-details { + max-width: 60em; + display: flex; + flex-direction: column; + flex-grow: 1; + background-color: $k-white; + padding: 15px; + border-radius: 10px; + height: calc(100vh - 110px - $case-top); + overflow-y: auto; + margin-right: 1em; } } \ No newline at end of file diff --git a/src/assets/styles/style.scss b/src/assets/styles/style.scss index 7e61ac1..37ddb19 100644 --- a/src/assets/styles/style.scss +++ b/src/assets/styles/style.scss @@ -369,7 +369,10 @@ textarea{ } .cases-table, -.recommendations-table { +.recommendations-table, +.resources-table { + width: 100%; + overflow-x: auto; display: flex; flex: 1; align-items: flex-start; @@ -511,4 +514,72 @@ textarea{ width: 0 !important; height: 0 !important; } +} + +.kpi { + display: flex; + flex-direction: column; + justify-content: space-around; + padding: 10px; + margin: 10px 10px 0px 0px; + background-color: $k-white; + box-shadow: 0px 10px 27px rgba(0, 0, 0, 0.05); + color: $k-black; + border-radius: 10px; + transition: 0.2s; + height: max-content; +} + +.kpi-number { + color: $k-blue; +} + +.show-details { + background: none; + border: none; + font-size: 14px; + color: #007bff; +} + +.resources-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + overflow-x: auto; + //min-width: 55rem; +} + +.resources-table th, +.resources-table td { + padding: 8px; + vertical-align: middle; + border-bottom: 1px solid #ccc; + border-top: none !important; + + th.sortable::after, th.vtl-asc::after, th.vtl-desc::after { + display: none !important; + } +} + +.fixed-width { + width: auto; + min-width: 13em; + text-align: left; +} + +.resource-status { + display: inline-block; + padding: 4px 8px; + text-align: center; + font-weight: bold; + border-radius: 7px; + width: 8em; +} + +.resource-status.available { + background: linear-gradient(135deg, $k-green1 5%, $k-green2 100%); +} + +.resource-status.unavailable { + background: linear-gradient(135deg, #AAAAAA 0%, #979797 100%); } \ No newline at end of file diff --git a/src/common/utils.js b/src/common/utils.js index 2baa624..7c71cb3 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -76,6 +76,14 @@ export default { pStatus: (p) => p.status, pIsRecommended: (p) => p.output.cate > 0 }, + 'RESOURCE_ALLOCATION': { + pType: 'Resource allocation', + pColor: 'background-green', + pText: (p) => `Allocate resource ${p.output.resource} until ${new Date(p.output.allocated_until).toLocaleString()}`, + pMetric: (p) => `Causal effect: ${p.output.cate_category} (${p.output.cate})`, + pStatus: (p) => p.status, + pIsRecommended: (p) => p.output.cate > 0 + } }, parseDuration(duration) { diff --git a/src/components/OngoingChartsManagerComponent.vue b/src/components/OngoingChartsManagerComponent.vue new file mode 100644 index 0000000..8f4b2a2 --- /dev/null +++ b/src/components/OngoingChartsManagerComponent.vue @@ -0,0 +1,121 @@ + + + \ No newline at end of file diff --git a/src/components/PositiveOutcomeItemComponent.vue b/src/components/PositiveOutcomeItemComponent.vue new file mode 100644 index 0000000..23813d3 --- /dev/null +++ b/src/components/PositiveOutcomeItemComponent.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/src/components/ResourcesTableComponent.vue b/src/components/ResourcesTableComponent.vue new file mode 100644 index 0000000..994051b --- /dev/null +++ b/src/components/ResourcesTableComponent.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/components/SideBarComponent.vue b/src/components/SideBarComponent.vue index cb760dc..3ae5f82 100644 --- a/src/components/SideBarComponent.vue +++ b/src/components/SideBarComponent.vue @@ -1,7 +1,7 @@ - \ No newline at end of file + diff --git a/src/components/casePage/FlowDiagramComponent.vue b/src/components/casePage/FlowDiagramComponent.vue index 691f369..55965bd 100644 --- a/src/components/casePage/FlowDiagramComponent.vue +++ b/src/components/casePage/FlowDiagramComponent.vue @@ -1,6 +1,6 @@ @@ -12,117 +12,118 @@ import dagre from 'cytoscape-dagre'; cytoscape.use(dagre); export default { - name: 'vue-cytoscape', props: { currentCase: Object, caseCompleted: Boolean, parameters: Object }, - data() { return { cy: null, elems: [], + uniqueId: `flow-cy-${Math.random().toString(36).substr(2, 9)}` } }, - mounted() { this.createDiagram(); }, - watch: { currentCase() { this.createNodes(); } }, - methods: { displayDiagram() { - this.cy.layout({ - fit: false, - name: 'dagre', - rankDir: 'LR', - align: 'DR', - }).run() - this.cy.zoom(1.2); + if (this.cy) { + this.cy.layout({ + fit: false, + name: 'dagre', + rankDir: 'LR', + align: 'DR', + }).run(); + this.cy.zoom(1.2); + } }, - createDiagram() { + // Wait until the DOM is fully rendered + this.$nextTick(() => { + const container = document.getElementById(this.uniqueId); + if (!container) { + console.error("Diagram container not found:", this.uniqueId); + return; + } - var width = 15; - var height = 15; - var lineWidth = 1; - var cy = cytoscape({ - container: document.getElementById('flow-cy'), - // zoomingEnabled: false, - // panningEnabled: false, - autoungrabify: true, - // autounselectify: true, - - style: [ - - { - selector: 'node', - style: { - 'text-halign': 'center', - 'text-valign': 'bottom', - 'text-margin-y': 5, - 'shape': 'ellipse', - 'background-color': '#d2d6da', - 'border-width': 0, - 'text-wrap': 'wrap', - 'text-max-width': width - 10, - 'height': height, - 'width': width, - 'font-size': 6, - 'font-family': 'arial' - } - }, - { - selector: 'edge', - style: { - 'curve-style': 'straight', - 'target-arrow-shape': 'vee', - 'width': lineWidth, - 'line-color': '#252F40', - 'target-arrow-color': '#252F40', - } - }, - { - selector: '.nextActivity', - style: { - 'label': 'data(label)', - 'background-color': '#EBF0FF', - } - }, + var width = 15; + var height = 15; + var lineWidth = 1; + var cy = cytoscape({ + container: container, + autoungrabify: true, + style: [ + { + selector: 'node', + style: { + 'text-halign': 'center', + 'text-valign': 'bottom', + 'text-margin-y': 5, + 'shape': 'ellipse', + 'background-color': '#d2d6da', + 'border-width': 0, + 'text-wrap': 'wrap', + 'text-max-width': width - 10, + 'height': height, + 'width': width, + 'font-size': 6, + 'font-family': 'arial' + } + }, + { + selector: 'edge', + style: { + 'curve-style': 'straight', + 'target-arrow-shape': 'vee', + 'width': lineWidth, + 'line-color': '#252F40', + 'target-arrow-color': '#252F40', + } + }, + { + selector: '.nextActivity', + style: { + 'label': 'data(label)', + 'background-color': '#EBF0FF', + } + }, + { + selector: '.completedActivity', + style: { + 'label': 'data(label)', + 'background-color': '#8392AB', + } + }, + ], + }); - { - selector: '.completedActivity', - style: { - 'label': 'data(label)', - 'background-color': '#8392AB', - } - }, - ], + this.cy = cy; + this.createNodes(); }); - - this.cy = cy; - this.createNodes(); - }, - createNodes() { + if (!this.cy || !this.currentCase.activities) { + console.error("Cytoscape instance or activities not available"); + return; + } + let activities = this.currentCase.activities; - // activities = activities.slice(-3); // display only last 3 completed activities const l = activities.length; var elems = []; - var lastNodeId = 'an' + (l - 1) + var lastNodeId = 'an' + (l - 1); for (let i = 0; i < l; i++) { const activity = activities[i]; - let content = activity[this.parameters.columnsDefinitionReverse['ACTIVITY']] + let content = activity[this.parameters.columnsDefinitionReverse['ACTIVITY']]; elems.push({ group: "nodes", @@ -144,6 +145,7 @@ export default { }); } } + let lastRecommendations = activities[l - 1].prescriptions; if (lastRecommendations) { let nextActivityRecommendation = lastRecommendations.filter(r => r.type === 'NEXT_ACTIVITY'); @@ -195,8 +197,6 @@ export default { this.elems = elems; this.displayDiagram(); } - } } - \ No newline at end of file diff --git a/src/components/casePage/ManagerViewComponent.vue b/src/components/casePage/ManagerViewComponent.vue new file mode 100644 index 0000000..7840d17 --- /dev/null +++ b/src/components/casePage/ManagerViewComponent.vue @@ -0,0 +1,130 @@ + + + \ No newline at end of file diff --git a/src/components/casePage/OperationalRecommendationManagerComponent.vue b/src/components/casePage/OperationalRecommendationManagerComponent.vue new file mode 100644 index 0000000..ba44668 --- /dev/null +++ b/src/components/casePage/OperationalRecommendationManagerComponent.vue @@ -0,0 +1,155 @@ + + + + diff --git a/src/components/casePage/RecommendationComponent.vue b/src/components/casePage/RecommendationComponent.vue index 29fe53e..350eea1 100644 --- a/src/components/casePage/RecommendationComponent.vue +++ b/src/components/casePage/RecommendationComponent.vue @@ -1,82 +1,80 @@ \ No newline at end of file diff --git a/src/components/casesPage/CompletedChartsComponent.vue b/src/components/casesPage/CompletedChartsComponent.vue index 5e713a7..e277aad 100644 --- a/src/components/casesPage/CompletedChartsComponent.vue +++ b/src/components/casesPage/CompletedChartsComponent.vue @@ -135,10 +135,10 @@ export default { name: "Accepted", data: [0, 0, 0], }, - { - name: 'Discarded', - data: [0, 0, 0] - } + { + name: 'Discarded', + data: [0, 0, 0] + } ], chartOptions: { colors: ['#17ad37', '#7e7e7e'], @@ -224,17 +224,21 @@ export default { createRecommendationsStatistics() { this.cases.forEach(({ case_performance, activities }) => { - const outcome = utils.calculateCaseOutcome(case_performance); const prescriptions = activities.map(a => a.prescriptions).flat(); prescriptions.forEach(p => { + if (!this.recommendationsStatistics.rows[outcome]) { + this.recommendationsStatistics.rows[outcome] = {}; + } + if (!this.recommendationsStatistics.rows[outcome][p.type]) { + this.recommendationsStatistics.rows[outcome][p.type] = { total: 0, accepted: 0 }; + } if (p.status === 'accepted') this.recommendationsStatistics.rows[outcome][p.type].accepted += 1; - this.recommendationsStatistics.rows[outcome][p.type].total += 1 + this.recommendationsStatistics.rows[outcome][p.type].total += 1; }); }); - - }, + } } } diff --git a/src/components/casesPage/OngoingChartsComponent.vue b/src/components/casesPage/OngoingChartsComponent.vue index c7afc41..01b29b4 100644 --- a/src/components/casesPage/OngoingChartsComponent.vue +++ b/src/components/casesPage/OngoingChartsComponent.vue @@ -15,7 +15,6 @@ \ No newline at end of file diff --git a/src/pages/CasePage.vue b/src/pages/CasePage.vue index 3ab4457..1077a58 100644 --- a/src/pages/CasePage.vue +++ b/src/pages/CasePage.vue @@ -48,19 +48,20 @@ - - + + + + - + \ No newline at end of file diff --git a/src/pages/CasesPage.vue b/src/pages/CasesPage.vue index bc9d482..e599f81 100644 --- a/src/pages/CasesPage.vue +++ b/src/pages/CasesPage.vue @@ -23,31 +23,57 @@ Cases without recommendations

{{ casesData.length - casesWithRecommendations }}

+ +
+
+
+
+ or +
+ and + +
+
+
+
+
- - - + +

Cases Overview Table

- +
- - - + + +
- + \ No newline at end of file diff --git a/src/pages/OngoingCasesManager.vue b/src/pages/OngoingCasesManager.vue new file mode 100644 index 0000000..3cd2654 --- /dev/null +++ b/src/pages/OngoingCasesManager.vue @@ -0,0 +1,205 @@ + + + \ No newline at end of file diff --git a/src/pages/RecommendationsPage.vue b/src/pages/RecommendationsPage.vue index 093c83c..e84265d 100644 --- a/src/pages/RecommendationsPage.vue +++ b/src/pages/RecommendationsPage.vue @@ -8,50 +8,78 @@
Total current recommendations
-

{{ formattedData.length }}

+

{{ formattedData.length }}

- - - -
- - - - - - - - + +
+ + - - \ No newline at end of file +}; + diff --git a/src/pages/ResourcesPage.vue b/src/pages/ResourcesPage.vue new file mode 100644 index 0000000..336a596 --- /dev/null +++ b/src/pages/ResourcesPage.vue @@ -0,0 +1,99 @@ + + + diff --git a/src/router.js b/src/router.js index 84360e2..162fe88 100644 --- a/src/router.js +++ b/src/router.js @@ -7,6 +7,7 @@ import DashBoardPage from './pages/DashBoardPage' import ParametersPage from './pages/ParametersPage' import ColumnsDefinitionPage from "./pages/ColumnsDefinitionPage" import RecommendationsPage from './pages/RecommendationsPage' +import ResourcesPage from './pages/ResourcesPage.vue'; const routes = [ { @@ -57,14 +58,19 @@ const routes = [ path: 'columns', name: 'columns', component: ColumnsDefinitionPage, - }, - { - path: "recommendations", - name: "recommendations", - component: RecommendationsPage, - }, + } ] }, + { + path: '/resources', + name: 'resources', + component: ResourcesPage, + }, + { + path: "/recommendations", + name: "recommendations", + component: RecommendationsPage, + }, ]; diff --git a/src/services/resources.service.js b/src/services/resources.service.js new file mode 100644 index 0000000..8ed2e49 --- /dev/null +++ b/src/services/resources.service.js @@ -0,0 +1,105 @@ +import casesService from "@/services/cases.service"; +import utils from "@/common/utils"; + +const getSystemTime = (cases) => { + const casesArray = Array.isArray(cases) ? cases : [cases]; + for (let event of casesArray) { + if (event.activities && Array.isArray(event.activities) && event.activities.length > 0) { + for (let activity of event.activities) { + if (activity.prescriptions && Array.isArray(activity.prescriptions) && activity.prescriptions.length > 0) { + console.log("Found system time in prescription:", activity.prescriptions[0].date); + return activity.prescriptions[0].date; + } + } + } + } + console.error("No valid system time found in cases."); + return null; +}; + +const processResourceData = (cases) => { + const systemTime = getSystemTime(cases); + if (!systemTime) { + console.error("System time is not set. Cannot process resource data."); + return []; + } + + const systemTimeDate = new Date(systemTime); + const casesArray = Array.isArray(cases) ? cases : [cases]; + + return casesArray.flatMap(event => { + if (!event.activities || !Array.isArray(event.activities) || event.activities.length === 0) { + return []; + } + + const eventSystemTime = new Date(event.activities[0]["time:timestamp"]); + + return event.activities.flatMap(activity => { + return activity.prescriptions + .filter(p => p.type === 'RESOURCE_ALLOCATION' && p.output && p.output.allocated_until) + .map(p => { + const currentSystemTime = eventSystemTime; + const timeDifference = systemTimeDate - currentSystemTime; + + const originalAllocatedUntilTime = new Date(p.output.allocated_until); + // Adjust allocated_until by subtracting the time difference + const adjustedAllocatedUntilTime = new Date(originalAllocatedUntilTime.getTime() - timeDifference); + const isBusy = adjustedAllocatedUntilTime > currentSystemTime; // Check if the resource is still allocated + + return { + id: event._id, + name: `${p.output.resource}`, + role: 'Dynamic Role', + status: isBusy ? 'Busy' : 'Available' + }; + }); + }); + }); +}; + +const fetchResourceData = async () => { + try { + const response = await casesService.getCasesByLogAndCompletion(utils.getLocal('logId'), 'ongoing'); + if (response.data && Array.isArray(response.data.cases) && response.data.cases.length > 0) { + return processResourceData(response.data.cases); + } else { + console.log("No cases data returned from API"); + return []; + } + } catch (error) { + console.error("Fetching error:", error); + throw error; + } +}; + +const fetchResourceDataByCaseId = async (caseId) => { + try { + console.log("Fetching cases with logId:", utils.getLocal('logId')); + const response = await casesService.getCasesByLogAndCompletion(utils.getLocal('logId'), 'ongoing'); + console.log("Response from API:", response.data); + + if (response.data && Array.isArray(response.data.cases)) { + console.log("Cases is an array with length:", response.data.cases.length); + const matchingCase = response.data.cases.find(c => c._id === caseId); + + if (matchingCase) { + console.log("Case ID matches:", caseId); + return processResourceData([matchingCase]); + } else { + console.log("No matching case ID found. Expected:", caseId); + return []; + } + } else { + console.log("Cases is not an array or is undefined. Cases:", response.data.cases); + return []; + } + } catch (error) { + console.error("Fetching error:", error); + throw error; + } +}; + +export default { + fetchResourceData, + fetchResourceDataByCaseId +}; \ No newline at end of file