
+++ client/views/component/chart/Chart11.jsx
... | ... | @@ -0,0 +1,156 @@ |
1 | +import React, { Component } from "react"; | |
2 | +import * as am5 from "@amcharts/amcharts5"; | |
3 | +import * as am5xy from "@amcharts/amcharts5/xy"; | |
4 | +import am5themes_Animated from "@amcharts/amcharts5/themes/Animated"; | |
5 | +import CommonUtil from "../../../resources/js/CommonUtil"; | |
6 | + | |
7 | +export default function Chart11({ data }) { | |
8 | + const createChart = () => { | |
9 | + console.log('createChart11 data : ', data); | |
10 | + | |
11 | + let root = am5.Root.new("Chart11"); | |
12 | + root._logo.dispose(); | |
13 | + // Set themes | |
14 | + // https://www.amcharts.com/docs/v5/concepts/themes/ | |
15 | + root.setThemes([ | |
16 | + am5themes_Animated.new(root) | |
17 | + ]); | |
18 | + | |
19 | + // Create chart | |
20 | + // https://www.amcharts.com/docs/v5/charts/xy-chart/ | |
21 | + let chart = root.container.children.push( | |
22 | + am5xy.XYChart.new(root, { | |
23 | + panX: true, | |
24 | + panY: true, | |
25 | + wheelX: "panX", | |
26 | + wheelY: "zoomX", | |
27 | + layout: root.verticalLayout, | |
28 | + pinchZoomX: true | |
29 | + }) | |
30 | + ); | |
31 | + | |
32 | + // Add cursor | |
33 | + // https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/ | |
34 | + let cursor = chart.set("cursor", am5xy.XYCursor.new(root, { | |
35 | + behavior: "none" | |
36 | + })); | |
37 | + cursor.lineY.set("visible", false); | |
38 | + | |
39 | + // Create axes | |
40 | + // https://www.amcharts.com/docs/v5/charts/xy-chart/axes/ | |
41 | + let xRenderer = am5xy.AxisRendererX.new(root, {}); | |
42 | + xRenderer.grid.template.set("location", 0.5); | |
43 | + xRenderer.labels.template.setAll({ | |
44 | + location: 0.5, | |
45 | + multiLocation: 0.5 | |
46 | + }); | |
47 | + | |
48 | + let xAxis = chart.xAxes.push( | |
49 | + am5xy.CategoryAxis.new(root, { | |
50 | + categoryField: "date", | |
51 | + renderer: xRenderer, | |
52 | + tooltip: am5.Tooltip.new(root, {}) | |
53 | + }) | |
54 | + ); | |
55 | + | |
56 | + xAxis.data.setAll(data); | |
57 | + | |
58 | + let yAxis = chart.yAxes.push( | |
59 | + am5xy.ValueAxis.new(root, { | |
60 | + maxPrecision: 0, | |
61 | + renderer: am5xy.AxisRendererY.new(root, { | |
62 | + inversed: false | |
63 | + }) | |
64 | + }) | |
65 | + ); | |
66 | + | |
67 | + // Add series | |
68 | + // https://www.amcharts.com/docs/v5/charts/xy-chart/series/ | |
69 | + | |
70 | + function createSeries(name, field) { | |
71 | + let series = chart.series.push( | |
72 | + am5xy.LineSeries.new(root, { | |
73 | + name: name, | |
74 | + xAxis: xAxis, | |
75 | + yAxis: yAxis, | |
76 | + valueYField: field, | |
77 | + categoryXField: "date", | |
78 | + tooltip: am5.Tooltip.new(root, { | |
79 | + pointerOrientation: "horizontal", | |
80 | + labelText: "[bold]{name}[/]\n{categoryX}: {valueY}" | |
81 | + }) | |
82 | + }) | |
83 | + ); | |
84 | + | |
85 | + | |
86 | + series.bullets.push(function () { | |
87 | + return am5.Bullet.new(root, { | |
88 | + sprite: am5.Circle.new(root, { | |
89 | + radius: 5, | |
90 | + fill: series.get("fill") | |
91 | + }) | |
92 | + }); | |
93 | + }); | |
94 | + | |
95 | + // create hover state for series and for mainContainer, so that when series is hovered, | |
96 | + // the state would be passed down to the strokes which are in mainContainer. | |
97 | + series.set("setStateOnChildren", true); | |
98 | + series.states.create("hover", {}); | |
99 | + | |
100 | + series.mainContainer.set("setStateOnChildren", true); | |
101 | + series.mainContainer.states.create("hover", {}); | |
102 | + | |
103 | + series.strokes.template.states.create("hover", { | |
104 | + strokeWidth: 4 | |
105 | + }); | |
106 | + | |
107 | + series.data.setAll(data); | |
108 | + series.appear(1000); | |
109 | + } | |
110 | + | |
111 | + createSeries("기기등록율", "register_percent"); | |
112 | + createSeries("데이터수집율", "data_percent"); | |
113 | + createSeries("복약율", "medication_percent"); | |
114 | + | |
115 | + // Add scrollbar | |
116 | + // https://www.amcharts.com/docs/v5/charts/xy-chart/scrollbars/ | |
117 | + chart.set("scrollbarX", am5.Scrollbar.new(root, { | |
118 | + orientation: "horizontal", | |
119 | + marginBottom: 20 | |
120 | + })); | |
121 | + | |
122 | + let legend = chart.children.push( | |
123 | + am5.Legend.new(root, { | |
124 | + centerX: am5.p50, | |
125 | + x: am5.p50 | |
126 | + }) | |
127 | + ); | |
128 | + | |
129 | + // Make series change state when legend item is hovered | |
130 | + legend.itemContainers.template.states.create("hover", {}); | |
131 | + | |
132 | + legend.itemContainers.template.events.on("pointerover", function (e) { | |
133 | + e.target.dataItem.dataContext.hover(); | |
134 | + }); | |
135 | + legend.itemContainers.template.events.on("pointerout", function (e) { | |
136 | + e.target.dataItem.dataContext.unhover(); | |
137 | + }); | |
138 | + | |
139 | + legend.data.setAll(chart.series.values); | |
140 | + | |
141 | + // Make stuff animate on load | |
142 | + // https://www.amcharts.com/docs/v5/concepts/animations/ | |
143 | + chart.appear(1000, 100); | |
144 | + } | |
145 | + | |
146 | + React.useEffect(() => { | |
147 | + console.log('React.useEffect Chart11 data : ', data); | |
148 | + if (CommonUtil.isEmpty(data) == false) { | |
149 | + createChart(); | |
150 | + } | |
151 | + }, [data]) | |
152 | + | |
153 | + return ( | |
154 | + <div id="Chart11" style={{ width: "100%", height: "100%" }}></div> | |
155 | + ) | |
156 | +}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/AppRoute.jsx
+++ client/views/pages/AppRoute.jsx
... | ... | @@ -36,6 +36,7 @@ |
36 | 36 |
import VisitSelectOne from "./visit/visit/VisitSelectOne.jsx"; |
37 | 37 |
import EquipmentRentalInsert from "./equipment/EquipmentRentalInsert.jsx"; |
38 | 38 |
import EquipmentSelect from "./equipment/EquipmentSelect.jsx"; |
39 |
+import EquipmentData from "./equipment/EquipmentData.jsx"; |
|
39 | 40 |
import GovernmentEquipmentSelect from "./equipment/GovernmentEquipmentSelect.jsx"; |
40 | 41 |
import AgencyEquipmentSelect from "./equipment/AgencyEquipmentSelect.jsx"; |
41 | 42 |
import EquipmentManagementSelectOne from "./equipment/EquipmentManagementSelectOne.jsx"; |
... | ... | @@ -144,6 +145,7 @@ |
144 | 145 |
<Route path="/EquipmentRentalInsert" element={<EquipmentRentalInsert />}></Route> |
145 | 146 |
<Route path="/EquipmentManagementInsert" element={<EquipmentManagementInsert />}></Route> |
146 | 147 |
<Route path="/EquipmentSelect" element={<EquipmentSelect />}></Route> |
148 |
+ <Route path="/EquipmentData" element={<EquipmentData />}></Route> |
|
147 | 149 |
<Route path="/EquipmentManagementSelectOne" element={<EquipmentManagementSelectOne />}></Route> |
148 | 150 |
<Route path="/MedicineCareSelectOne" element={<MedicineCareSelectOne />}></Route> |
149 | 151 |
<Route path="/TemperatureManagementSelectOne" element={<TemperatureManagementSelectOne />}></Route> |
+++ client/views/pages/equipment/EquipmentData.jsx
... | ... | @@ -0,0 +1,269 @@ |
1 | +import React from "react"; | |
2 | +import { useNavigate, useLocation } from "react-router"; | |
3 | +import { useSelector } from "react-redux"; | |
4 | +import SubTitle from "../../component/SubTitle.jsx"; | |
5 | +import Modal from "../../component/Modal.jsx"; | |
6 | + | |
7 | +import House from "../../../resources/files/icon/house.png"; | |
8 | +import Arrow from "../../../resources/files/icon/arrow.png"; | |
9 | +import Pagination from "../../component/Pagination.jsx"; | |
10 | +import Chart11 from "../../component/chart/Chart11.jsx"; | |
11 | + | |
12 | +import CommonUtil from "../../../resources/js/CommonUtil.js"; | |
13 | + | |
14 | +import DatePicker from 'react-datepicker'; | |
15 | +import 'react-datepicker/dist/react-datepicker.css'; | |
16 | + | |
17 | +export default function EquipmentData() { | |
18 | + const navigate = useNavigate(); | |
19 | + const location = useLocation(); | |
20 | + | |
21 | + //전역 변수 저장 객체 | |
22 | + const state = useSelector((state) => { return state }); | |
23 | + const defaultGovernmentId = CommonUtil.isEmpty(location.state) || CommonUtil.isEmpty(location.state['government_id']) ? null : location.state['government_id']; | |
24 | + | |
25 | + //기관 계층 구조 목록 | |
26 | + const [orgListOfHierarchy, setOrgListOfHierarchy] = React.useState([]); | |
27 | + //기관(관리, 시행) 계층 구조 목록 조회 | |
28 | + const orgSelectListOfHierarchy = () => { | |
29 | + fetch("/org/orgSelectListOfHierarchy.json", { | |
30 | + method: "POST", | |
31 | + headers: { | |
32 | + 'Content-Type': 'application/json; charset=UTF-8' | |
33 | + }, | |
34 | + body: JSON.stringify({ 'government_id': defaultGovernmentId }), | |
35 | + }).then((response) => response.json()).then((data) => { | |
36 | + console.log("기관(관리, 시행) 계층 구조 목록 조회 : ", data); | |
37 | + setOrgListOfHierarchy(data); | |
38 | + }).catch((error) => { | |
39 | + console.log('orgSelectListOfHierarchy() /org/orgSelectListOfHierarchy.json error : ', error); | |
40 | + }); | |
41 | + }; | |
42 | + //검색 변수 (초기화값) | |
43 | + const [userSearch, setUserSearch] = React.useState({ | |
44 | + 'government_id': null, | |
45 | + 'agency_id': null, | |
46 | + | |
47 | + 'currentPage': 1, | |
48 | + 'perPage': 10, | |
49 | + }); | |
50 | + | |
51 | + //올잇메디 선택 | |
52 | + const adminChange = () => { | |
53 | + const newUserSearch = JSON.parse(JSON.stringify(userSearch)); | |
54 | + newUserSearch['government_id'] = null; | |
55 | + newUserSearch['agency_id'] = null; | |
56 | + setUserSearch(newUserSearch); | |
57 | + equipmentDataList(); | |
58 | + } | |
59 | + | |
60 | + //관리 기관 선택 | |
61 | + const governmentChange = (government_id) => { | |
62 | + const newUserSearch = JSON.parse(JSON.stringify(userSearch)); | |
63 | + newUserSearch['government_id'] = government_id; | |
64 | + newUserSearch['agency_id'] = null; | |
65 | + setUserSearch(newUserSearch); | |
66 | + equipmentDataList(); | |
67 | + } | |
68 | + | |
69 | + //시행 기관 선택 | |
70 | + const agencyChange = (government_id, agency_id) => { | |
71 | + const newUserSearch = JSON.parse(JSON.stringify(userSearch)); | |
72 | + newUserSearch['government_id'] = government_id; | |
73 | + newUserSearch['agency_id'] = agency_id; | |
74 | + setUserSearch(newUserSearch); | |
75 | + equipmentDataList(); | |
76 | + } | |
77 | + | |
78 | + const [isSelectDate, setIsSelectDate] = React.useState(false); | |
79 | + const [startDate, setStartDate] = React.useState(new Date('2023-08-01')); | |
80 | + const [endDate, setEndDate] = React.useState(new Date()); | |
81 | + | |
82 | + //통계 자료 데이터 | |
83 | + const [equipmentData, setEquipmentData] = React.useState([]); | |
84 | + //복약 현황 조회 | |
85 | + const equipmentDataList = () => { | |
86 | + console.log("시작날짜 : ", startDate); | |
87 | + console.log("끝난날짜 : ", endDate); | |
88 | + | |
89 | + var dateList = []; | |
90 | + var curDate = new Date(startDate); | |
91 | + while (curDate <= new Date(endDate)) { | |
92 | + dateList.push({ government_id: userSearch['government_id'], agency_id: userSearch['agency_id'], receive_datetime: curDate.toISOString().split("T")[0] }); | |
93 | + curDate.setDate(curDate.getDate() + 1); | |
94 | + } | |
95 | + | |
96 | + const reverse = dateList.reverse(); | |
97 | + | |
98 | + var dataListArry = []; | |
99 | + for (let i = 0; i < reverse.length; i++) { | |
100 | + fetch("/stats/equipmentDataList.json", { | |
101 | + method: "POST", | |
102 | + headers: { | |
103 | + 'Content-Type': 'application/json; charset=UTF-8' | |
104 | + }, | |
105 | + body: JSON.stringify(reverse[i]), | |
106 | + }).then((response) => response.json()).then((data) => { | |
107 | + dataListArry[i] = { | |
108 | + date: new Date(data['stockList'][0]['datetime']), | |
109 | + datetime: data['stockList'][0]['datetime'], | |
110 | + delivery_equipment: data['stockList'][0]['delivery_equipment'], | |
111 | + register_equipment: data['stockList'][0]['register_equipment'], | |
112 | + data_equipment: data['dataList'][0]['data_equipment'], | |
113 | + medication_equipment: data['dataList'][0]['medication_equipment'], | |
114 | + register_percent: Math.floor((data['stockList'][0]['register_equipment'] / data['stockList'][0]['delivery_equipment']) * 100), | |
115 | + data_percent: Math.floor((data['dataList'][0]['data_equipment'] / data['stockList'][0]['delivery_equipment']) * 100), | |
116 | + medication_percent: Math.floor((data['dataList'][0]['medication_equipment'] / data['stockList'][0]['delivery_equipment']) * 100) | |
117 | + }; | |
118 | + }).catch((error) => { | |
119 | + console.log('equipmentDataList() /stats/equipmentDataList.json error : ', error); | |
120 | + }); | |
121 | + } | |
122 | + console.log('dataListArry : ', dataListArry) | |
123 | + setEquipmentData(dataListArry); | |
124 | + console.log("equipmentData : ", equipmentData) | |
125 | + }; | |
126 | + | |
127 | + const searchData = () => { | |
128 | + equipmentDataList(); | |
129 | + } | |
130 | + | |
131 | + React.useEffect(() => { | |
132 | + orgSelectListOfHierarchy(); | |
133 | + equipmentDataList(); | |
134 | + }, []); | |
135 | + | |
136 | + return ( | |
137 | + <main> | |
138 | + <div className="content-wrap"> | |
139 | + <div className="search-management flex margin-bottom2 margin-top gap"> | |
140 | + <select style={{ maxWidth: '150px' }} | |
141 | + onChange={(e) => { setIsSelectDate(e.target.value == 'selectDate' ? false : true) }} | |
142 | + > | |
143 | + <option value="selectDate">날짜선택</option> | |
144 | + <option value="selectDatetime">기간선택</option> | |
145 | + </select> | |
146 | + {!isSelectDate ? | |
147 | + <div className="selectDate" style={{ width: 'calc(100% - 300px)' }}> | |
148 | + <input type="date" | |
149 | + value={endDate} | |
150 | + onChange={(e) => { setEndDate(e.target.value) }} | |
151 | + onKeyDown={function (e) { e.preventDefault() }} /> | |
152 | + </div> | |
153 | + : | |
154 | + <div className="selectDatetime flex" style={{ width: 'calc(100% - 300px)' }}> | |
155 | + <input type="date" | |
156 | + value={startDate} | |
157 | + onChange={(e) => { setStartDate(e.target.value) }} | |
158 | + onKeyDown={function (e) { e.preventDefault() }} /> | |
159 | + <input type="date" | |
160 | + value={endDate} | |
161 | + onChange={(e) => { setEndDate(e.target.value) }} | |
162 | + onKeyDown={function (e) { e.preventDefault() }} /> | |
163 | + </div> | |
164 | + } | |
165 | + <button className={"btn-small gray-btn"} style={{ maxWidth: '150px' }} | |
166 | + onClick={() => { searchData(); }} | |
167 | + >검색</button> | |
168 | + </div> | |
169 | + | |
170 | + <div> | |
171 | + <div className="flex-align-start userauthoriylist gap5"> | |
172 | + <div className="left"> | |
173 | + <SubTitle | |
174 | + explanation={"기관 리스트"} | |
175 | + className="margin-bottom" | |
176 | + /> | |
177 | + {/* 카테고리 디자인 필요 (a.active 클래스 필요) */} | |
178 | + <div style={{ fontSize: '16px' }} className="category"> | |
179 | + {state.loginUser['authority'] == 'ROLE_ADMIN' ? | |
180 | + <a onClick={adminChange} | |
181 | + className={userSearch['government_id'] == null && userSearch['agency_id'] == null ? "active" : ""}> | |
182 | + 올잇메디 | |
183 | + </a> | |
184 | + : null} | |
185 | + <ul style={{ marginLeft: '15px' }}> | |
186 | + {orgListOfHierarchy.map((item, idx) => { | |
187 | + return ( | |
188 | + <li style={{ margin: '10px 0px' }} key={idx}> | |
189 | + <span style={{ marginRight: '5px' }}><img src={House} alt="" /></span> | |
190 | + <a onClick={() => { governmentChange(item['government_id']) }} | |
191 | + className={item['government_id'] == userSearch['government_id'] ? "active" : ""}> | |
192 | + {item['government_name']} | |
193 | + </a> | |
194 | + {item['agencyList'] != undefined && item['agencyList'] != null ? | |
195 | + <ul style={{ marginLeft: '15px' }}> | |
196 | + {item['agencyList'].map((item2, idx2) => { | |
197 | + return ( | |
198 | + <li style={{ margin: '10px 0px' }} key={idx2}> | |
199 | + <span style={{ marginRight: '5px' }}><img src={Arrow} alt="" /></span> | |
200 | + <a onClick={() => { agencyChange(item['government_id'], item2['agency_id']) }} | |
201 | + className={item2['agency_id'] == userSearch['agency_id'] ? "active" : ""}> | |
202 | + {item2['agency_name']} | |
203 | + </a> | |
204 | + </li> | |
205 | + ) | |
206 | + })} | |
207 | + </ul> | |
208 | + : null | |
209 | + } | |
210 | + </li> | |
211 | + ) | |
212 | + })} | |
213 | + </ul> | |
214 | + </div> | |
215 | + </div> | |
216 | + <div className="right"> | |
217 | + <div className="flex equip-tab"> | |
218 | + <SubTitle explanation={"선택한 기관의 통계 자료입니다."} /> | |
219 | + </div> | |
220 | + | |
221 | + <div style={{ height: '30vh' }}> | |
222 | + {equipmentData.length > 0 ? | |
223 | + <Chart11 data={equipmentData} /> | |
224 | + : "데이터가 없습니다."} | |
225 | + </div> | |
226 | + | |
227 | + <table class="caregiver-user protector-user"> | |
228 | + <thead> | |
229 | + <tr> | |
230 | + <th>날짜</th> | |
231 | + <th>납품기기</th> | |
232 | + <th>등록기기</th> | |
233 | + <th>데이터수집기기</th> | |
234 | + <th>복약기기</th> | |
235 | + <th>기기등록율(%)</th> | |
236 | + <th>데이터수집율(%)</th> | |
237 | + <th>복약율(%)</th> | |
238 | + </tr> | |
239 | + </thead> | |
240 | + <tbody> | |
241 | + {equipmentData.map((item, idx) => { | |
242 | + return ( | |
243 | + <tr key={idx}> | |
244 | + <td data-label="날짜">{item['datetime']}</td> | |
245 | + <td data-label="납품기기">{item['delivery_equipment']}</td> | |
246 | + <td data-label="등록기기">{item['register_equipment']}</td> | |
247 | + <td data-label="데이터수집기기">{item['data_equipment']}</td> | |
248 | + <td data-label="복약기기">{item['medication_equipment']}</td> | |
249 | + <td data-label="기기등록율">{item['register_percent']}%</td> | |
250 | + <td data-label="데이터수집율">{item['data_percent']}%</td> | |
251 | + <td data-label="복약율">{item['medication_percent']}%</td> | |
252 | + </tr> | |
253 | + ) | |
254 | + }) | |
255 | + } | |
256 | + {equipmentData == null || equipmentData.length == 0 ? | |
257 | + <tr> | |
258 | + <td colSpan={8}>조회된 데이터가 없습니다</td> | |
259 | + </tr> | |
260 | + : null} | |
261 | + </tbody> | |
262 | + </table> | |
263 | + </div> | |
264 | + </div> | |
265 | + </div> | |
266 | + </div> | |
267 | + </main > | |
268 | + ); | |
269 | +}(파일 끝에 줄바꿈 문자 없음) |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?