Browse Source

통계 컴포넌트 분리

pull/2/head
hhjk00 10 months ago
parent
commit
f3be389a2b
  1. 368
      src/components/statistics/StatisticsSearch.js
  2. 69
      src/components/statistics/StatisticsTotal.js
  3. 536
      src/containers/statistics/AbnormalSituationContainer.js
  4. 473
      src/containers/statistics/FlightContainer.js
  5. 455
      src/containers/statistics/FlightResultContainer.js

368
src/components/statistics/StatisticsSearch.js

@ -0,0 +1,368 @@
import {
Col,
Row,
Card,
CardHeader,
CardTitle,
CardBody,
CustomInput
} from 'reactstrap';
import { Search } from 'react-feather';
import { Bar, Doughnut } from 'react-chartjs-2';
export default function StatisticsSearch({
tooltipShadow,
gridLineColor,
labelColor,
searchData,
searchType,
dateLists,
categoryTypeOptions,
handlerBarTicks,
handlerTitleName,
handleChangeSearchType
}) {
const options = {
elements: {
rectangle: {
borderWidth: 2,
borderSkipped: 'bottom'
}
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 500,
legend: {
display: false
},
tooltips: {
// Updated default tooltip UI
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
},
scales: {
xAxes: [
{
display: true,
gridLines: {
display: true,
color: gridLineColor,
zeroLineColor: gridLineColor
},
scaleLabel: {
display: false
},
ticks: {
fontColor: labelColor
}
}
],
yAxes: [
{
display: true,
gridLines: {
color: gridLineColor,
zeroLineColor: gridLineColor
},
ticks: {
...handlerBarTicks(),
min: 0,
fontColor: labelColor
}
}
]
}
},
data = {
labels: searchData.graphData.map(i => i.name),
datasets: [
{
data: searchData.graphData.map(i => i.value),
backgroundColor: '#00bcd4',
borderColor: '#00bcd4',
barThickness: 15
}
]
};
const options2 = {
responsive: true,
maintainAspectRatio: true,
responsiveAnimationDuration: 500,
cutoutPercentage: 60,
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
padding: 18,
boxWidth: 8,
fontColor: labelColor,
fontSize: 14,
fontWeight: 500,
fontFamily: ['Rubik', 'Montserrat', 'NotoSansKR']
}
},
tooltips: {
callbacks: {
label(tooltipItem, data) {
const label = data.datasets[0].labels[tooltipItem.index] || '',
value = data.datasets[0].data[tooltipItem.index];
const output = ` ${label} : ${value} %`;
return output;
}
},
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
}
},
data2 = {
labels: searchData.topData.map(i => i.name),
datasets: [
{
labels: searchData.topData.map(i => i.name),
data: searchData.topData.map(i => i.value),
// 레드버전
// backgroundColor: [
// '#ffe8d1',
// '#ffb59e',
// '#f0826b',
// '#Bd4f38',
// '#8a1c05'
// ],
backgroundColor: [
'#ccffff',
'#99ffff',
'#66ffff',
'#33efff',
'#00bcd4'
],
borderWidth: 0,
pointStyle: 'rectRounded'
}
]
};
return (
<>
<div>
<Row>
<Col>
<div className='mt-2 cont-ti d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row'>
<div>
<h4>검색조건</h4>
</div>
{/* <div className='d-flex align-items-center'>
<Button.Ripple color='primary' size='sm'>
<Search size={16} />
검색
</Button.Ripple>
</div> */}
</div>
<Card>
<CardBody className='pal-card-body'>
<div className='search-cont'>
<dl>
<dt>
<div className='search-box'>
<div className='search-list-ti'>검색조건</div>
<div className='search-list'>
<div className='search-list-cont'>
<Row>
<Col className='list-input' md='4' sm='12'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
onChange={e =>
handleChangeSearchType(
'category',
e.target.value
)
}
>
{Object.entries(categoryTypeOptions).map(
([value, label]) => (
<option key={value} value={value}>
{label}
</option>
)
)}
</CustomInput>
</Col>
</Row>
</div>
</div>
</div>
</dt>
</dl>
</div>
</CardBody>
</Card>
</Col>
</Row>
</div>
<div>
<Row className='mt-2'>
<Col md='8' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} 통계
</CardTitle>
<div className='select-date-wrap'>
<Row>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.type}
onChange={e =>
handleChangeSearchType('dateType', e.target.value)
}
>
<option value='year'></option>
<option value='month'></option>
<option value='day'></option>
<option value='one-day'>시간</option>
</CustomInput>
</div>
{searchType.dateType === 'month' ||
searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.year}
onChange={e =>
handleChangeSearchType('year', e.target.value)
}
>
<option>2023</option>
</CustomInput>
</div>
{searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.month}
onChange={e =>
handleChangeSearchType('month', e.target.value)
}
>
{dateLists.month.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
{searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.day}
onChange={e =>
handleChangeSearchType('day', e.target.value)
}
>
{dateLists.day.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
</>
) : null}
</Row>
{/* <Calendar size={14} />
<Flatpickr
options={{
mode: 'range',
defaultDate: ['2019-05-01', '2019-05-10']
}}
className='form-control flat-picker bg-transparent border-0 shadow-none'
/> */}
</div>
</CardHeader>
<CardBody>
<div style={{ height: '400px' }}>
<Bar
data={data}
options={options}
height={400}
plugins={{
afterDraw: function (chart) {
if (data.datasets[0].data.length <= 0) {
let ctx = chart.chart.ctx;
let width = chart.chart.width;
let height = chart.chart.height;
chart.clear();
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
'표시할 데이터가 없습니다.',
width / 2,
height / 2
);
ctx.restore();
}
}
}}
/>
</div>
</CardBody>
</Card>
</Col>
<Col md='4' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} TOP5
</CardTitle>
</CardHeader>
<CardBody>
<div style={{ height: '275px' }}>
<Doughnut data={data2} options={options2} height={275} />
</div>
{/* <div className='d-flex justify-content-between mt-3 mb-1'>
<div className='d-flex align-items-center'></div>
</div> */}
</CardBody>
</Card>
</Col>
</Row>
</div>
</>
);
}

69
src/components/statistics/StatisticsTotal.js

@ -0,0 +1,69 @@
import { Col, Row } from 'reactstrap';
import { FcAlarmClock, FcWorkflow, FcBarChart } from 'react-icons/fc';
export default function StatisticsTotal({
titleName,
totalData,
totalTitle,
parseTime,
addCommasToNumber
}) {
const renderIcon = idx => {
if (idx === 0) return <FcAlarmClock />;
if (idx === 1) return <FcWorkflow />;
return <FcBarChart />;
};
const renderData = (data, titleName, idx) => {
if (titleName === '비행 통계') {
if (idx === 0) return parseTime(data);
if (idx === 1) return <>{addCommasToNumber(data)}m</>;
return <>{addCommasToNumber(data)}</>;
} else {
return <>{addCommasToNumber(data)}</>;
}
};
return (
<div>
<Row>
{totalData.map((i, idx) => (
<Col md='4' key={idx}>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
<span>{renderIcon(idx)}</span>
<span>{totalTitle[idx]}</span>
<span>{i.name}</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>
{renderData(i.year, titleName, idx)}
</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>
{renderData(i.month, titleName, idx)}
</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>
{renderData(i.day, titleName, idx)}
</span>
</td>
</tr>
</table>
</div>
</Col>
))}
</Row>
</div>
);
}

536
src/containers/statistics/AbnormalSituationContainer.js

@ -1,26 +1,11 @@
import { CustomMainLayout } from '../../components/layout/CustomMainLayout';
import {
Button,
Col,
Row,
Card,
CardHeader,
CardTitle,
CardBody,
CustomInput
} from 'reactstrap';
import { Search } from 'react-feather';
import { FcAlarmClock, FcWorkflow, FcBarChart } from 'react-icons/fc';
import { Bar, Doughnut } from 'react-chartjs-2';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as StcsActions from '../../modules/statistics/actions';
import StatisticsSearch from '../../components/statistics/StatisticsSearch';
import StatisticsTotal from '../../components/statistics/StatisticsTotal';
export default function AbnormalSituationContainer({
tooltipShadow,
gridLineColor,
labelColor
}) {
export default function AbnormalSituationContainer() {
const dispatch = useDispatch();
const { abnormal, abnormalSearch } = useSelector(
state => state.statisticsState
@ -39,6 +24,12 @@ export default function AbnormalSituationContainer({
});
const titleName = '비정상상황 통계';
const totalTitle = ['비행경로이탈', '비정상고도', '충돌위험'];
const categoryTypeOptions = {
PLAN: '비행경로이탈',
ALTITUDE: '비정상고도',
CRASH: '충돌위험'
};
useEffect(() => {
dispatch(StcsActions.ABNORMAL_STCS.request());
@ -84,12 +75,7 @@ export default function AbnormalSituationContainer({
// 그래프 타이틀 handler
const handlerTitleName = category => {
const categoryMappings = {
PLAN: '비행경로이탈',
ALTITUDE: '비정상고도',
CRASH: '충돌위험'
};
return categoryMappings[category];
return categoryTypeOptions[category];
};
// Bar Graph handler
@ -106,501 +92,29 @@ export default function AbnormalSituationContainer({
return 10 ** (exponent - 1);
};
// 123456789 -> 123,456,789
const addCommasToNumber = number => {
if (number === 'noData') return 0;
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const options = {
elements: {
rectangle: {
borderWidth: 2,
borderSkipped: 'bottom'
}
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 500,
legend: {
display: false
},
tooltips: {
// Updated default tooltip UI
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
},
scales: {
xAxes: [
{
display: true,
gridLines: {
display: true,
color: gridLineColor,
zeroLineColor: gridLineColor
},
scaleLabel: {
display: false
},
ticks: {
fontColor: labelColor
}
}
],
yAxes: [
{
display: true,
gridLines: {
color: gridLineColor,
zeroLineColor: gridLineColor
},
ticks: {
...handlerBarTicks(),
min: 0,
fontColor: labelColor
}
}
]
}
},
data = {
labels: abnormalSearch.graphData.map(i => i.name),
datasets: [
{
data: abnormalSearch.graphData.map(i => i.value),
backgroundColor: '#00bcd4',
borderColor: '#00bcd4',
barThickness: 15
}
]
};
const options2 = {
responsive: true,
maintainAspectRatio: true,
responsiveAnimationDuration: 500,
cutoutPercentage: 60,
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
padding: 18,
boxWidth: 8,
fontColor: labelColor,
fontSize: 14,
fontWeight: 500,
fontFamily: ['Rubik', 'Montserrat', 'NotoSansKR']
}
},
tooltips: {
callbacks: {
label(tooltipItem, data) {
const label = data.datasets[0].labels[tooltipItem.index] || '',
value = data.datasets[0].data[tooltipItem.index];
const output = ` ${label} : ${value}`;
return output;
}
},
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
}
},
data2 = {
labels: abnormalSearch.topData.map(i => i.name),
datasets: [
{
labels: abnormalSearch.topData.map(i => i.name),
data: abnormalSearch.topData.map(i => i.value),
//레드버전
// backgroundColor: [
// '#ffe8d1',
// '#ffb59e',
// '#f0826b',
// '#Bd4f38',
// '#8a1c05'
// ],
backgroundColor: [
'#ccffff',
'#99ffff',
'#66ffff',
'#33efff',
'#00bcd4'
],
borderWidth: 0,
pointStyle: 'rectRounded'
}
]
};
return (
<CustomMainLayout title={titleName}>
<div className='pal-card-box statistics'>
<div>
<Row>
{abnormal.map((i, idx) => (
<Col md='4' key={idx}>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
{idx === 0 ? (
<>
<span>
<FcAlarmClock />
</span>
<span>비행경로이탈</span>
</>
) : idx === 1 ? (
<>
<span>
<FcWorkflow />
</span>
<span>비정상고도</span>
</>
) : (
<>
<span>
<FcBarChart />
</span>
<span>충돌위험</span>
</>
)}
<span>{i.name}</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>
{addCommasToNumber(i.year)}
</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>
{addCommasToNumber(i.month)}
</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>
{addCommasToNumber(i.day)}
</span>
</td>
</tr>
</table>
</div>
</Col>
))}
{/* <Col md='4'>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
<span>
<FcAlarmClock />
</span>
<span>비행경로이탈</span>
<span>PA0001</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>8 10시간 35 12</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>1 35 12</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>35 12</span>
</td>
</tr>
</table>
</div>
</Col>
<Col md='4'>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
<span>
<FcWorkflow />
</span>
<span>비정상 고도</span>
<span>PA0002</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>10,845m</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>1,201m</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>53m</span>
</td>
</tr>
</table>
</div>
</Col>
<Col md='4'>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
<span>
<FcBarChart />
</span>
<span>충돌위험</span>
<span>PA0002</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>1,024,845</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>111,201</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>153</span>
</td>
</tr>
</table>
</div>
</Col> */}
</Row>
</div>
<div>
<Row>
<Col>
<div className='mt-2 cont-ti d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row'>
<div>
<h4>검색조건</h4>
</div>
{/* <div className='d-flex align-items-center'>
<Button.Ripple color='primary' size='sm'>
<Search size={16} />
검색
</Button.Ripple>
</div> */}
</div>
<Card>
<CardBody className='pal-card-body'>
<div className='search-cont'>
<dl>
<dt>
<div className='search-box'>
<div className='search-list-ti'>검색조건</div>
<div className='search-list'>
<div className='search-list-cont'>
<Row>
<Col className='list-input' md='4' sm='12'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
onChange={e =>
handleChangeSearchType(
'category',
e.target.value
)
}
>
<option value={'PLAN'}>비행경로이탈</option>
<option value={'ALTITUDE'}>
비정상고도
</option>
<option value={'CRASH'}>충돌위험</option>
</CustomInput>
</Col>
</Row>
</div>
</div>
</div>
</dt>
</dl>
</div>
</CardBody>
</Card>
</Col>
</Row>
</div>
<div>
<Row className='mt-2'>
<Col md='8' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} 통계
</CardTitle>
<div className='select-date-wrap'>
<Row>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.type}
onChange={e =>
handleChangeSearchType('dateType', e.target.value)
}
>
<option value='year'></option>
<option value='month'></option>
<option value='day'></option>
<option value='one-day'>시간</option>
</CustomInput>
</div>
{searchType.dateType === 'month' ||
searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.year}
onChange={e =>
handleChangeSearchType('year', e.target.value)
}
>
<option>2023</option>
</CustomInput>
</div>
{searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.month}
onChange={e =>
handleChangeSearchType(
'month',
e.target.value
)
}
>
{dateLists.month.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
{searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.day}
onChange={e =>
handleChangeSearchType('day', e.target.value)
}
>
{dateLists.day.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
</>
) : null}
</Row>
{/* <Calendar size={14} />
<Flatpickr
options={{
mode: 'range',
defaultDate: ['2019-05-01', '2019-05-10']
}}
className='form-control flat-picker bg-transparent border-0 shadow-none'
/> */}
</div>
</CardHeader>
<CardBody>
<div style={{ height: '400px' }}>
<Bar
data={data}
options={options}
height={400}
plugins={{
afterDraw: function (chart) {
if (data.length <= 0) {
let ctx = chart.chart.ctx;
let width = chart.chart.width;
let height = chart.chart.height;
chart.clear();
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
'표시할 데이터가 없습니다.',
width / 2,
height / 2
);
ctx.restore();
}
}
}}
/>
</div>
</CardBody>
</Card>
</Col>
<Col md='4' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} TOP5
</CardTitle>
</CardHeader>
<CardBody>
<div style={{ height: '275px' }}>
<Doughnut data={data2} options={options2} height={275} />
</div>
{/* <div className='d-flex justify-content-between mt-3 mb-1'>
<div className='d-flex align-items-center'></div>
</div> */}
</CardBody>
</Card>
</Col>
</Row>
</div>
<StatisticsTotal
totalTitle={totalTitle}
totalData={abnormal}
addCommasToNumber={addCommasToNumber}
/>
<StatisticsSearch
searchData={abnormalSearch}
searchType={searchType}
categoryTypeOptions={categoryTypeOptions}
dateLists={dateLists}
handlerBarTicks={handlerBarTicks}
handlerTitleName={handlerTitleName}
handleChangeSearchType={handleChangeSearchType}
/>
</div>
</CustomMainLayout>
);

473
src/containers/statistics/FlightContainer.js

@ -1,26 +1,11 @@
import { CustomMainLayout } from '../../components/layout/CustomMainLayout';
import {
Button,
Col,
Row,
Card,
CardHeader,
CardTitle,
CardBody,
CustomInput
} from 'reactstrap';
import { Search } from 'react-feather';
import { FcAlarmClock, FcWorkflow, FcBarChart } from 'react-icons/fc';
import { Bar, Doughnut } from 'react-chartjs-2';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as StcsActions from '../../modules/statistics/actions';
import StatisticsTotal from '../../components/statistics/StatisticsTotal';
import StatisticsSearch from '../../components/statistics/StatisticsSearch';
export default function FlightContainer({
tooltipShadow,
gridLineColor,
labelColor
}) {
export default function FlightContainer() {
const dispatch = useDispatch();
const { flight, flightSearch } = useSelector(state => state.statisticsState);
@ -37,6 +22,12 @@ export default function FlightContainer({
});
const titleName = '비행 통계';
const totalTitle = ['총 비행시간', '총 비행거리', '총 비행횟수'];
const categoryTypeOptions = {
TIME: '비행 시간',
DISTANCE: '비행 거리',
FLT_COUNT: '비행 횟수'
};
useEffect(() => {
dispatch(StcsActions.FLIGHT_STCS.request());
@ -83,17 +74,12 @@ export default function FlightContainer({
// 그래프 타이틀 handler
const handlerTitleName = category => {
const categoryMappings = {
TIME: '비행 시간',
DISTANCE: '비행 거리',
FLT_COUNT: '비행 횟수'
};
return categoryMappings[category];
return categoryTypeOptions[category];
};
// Bar Graph handler
const handlerBarTicks = () => {
const data = flightSearch.topData.map(i => i.value);
const data = flightSearch.graphData.map(i => i.value);
const max = Math.ceil(Math.max(...data) / 10) * 10;
const stepSize = handlerStepSize(max);
@ -105,15 +91,16 @@ export default function FlightContainer({
return 10 ** (exponent - 1);
};
// 123456789 -> 123,456,789
const addCommasToNumber = number => {
if (number === 'noData') return 0;
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
// '24:35:12' -> 1일 35분 12초
const parseTime = time => {
if (time === 'noData') {
return '0초';
}
if (time === 'noData') return '0초';
const [hour, minute, second] = time.split(':').map(Number);
@ -129,423 +116,25 @@ export default function FlightContainer({
return parts.join(' ');
};
const options = {
elements: {
rectangle: {
borderWidth: 2,
borderSkipped: 'bottom'
}
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 500,
legend: {
display: false
},
tooltips: {
// Updated default tooltip UI
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
},
scales: {
xAxes: [
{
display: true,
gridLines: {
display: true,
color: gridLineColor,
zeroLineColor: gridLineColor
},
scaleLabel: {
display: false
},
ticks: {
fontColor: labelColor
}
}
],
yAxes: [
{
display: true,
gridLines: {
color: gridLineColor,
zeroLineColor: gridLineColor
},
ticks: {
...handlerBarTicks(),
min: 0,
fontColor: labelColor
}
}
]
}
},
data = {
labels: flightSearch.graphData.map(i => i.name),
datasets: [
{
data: flightSearch.graphData.map(i => i.value),
backgroundColor: '#00bcd4',
borderColor: '#00bcd4',
barThickness: 15
}
]
};
const options2 = {
responsive: true,
maintainAspectRatio: true,
responsiveAnimationDuration: 500,
cutoutPercentage: 60,
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
padding: 18,
boxWidth: 8,
fontColor: labelColor,
fontSize: 14,
fontWeight: 500,
fontFamily: ['Rubik', 'Montserrat', 'NotoSansKR']
}
},
tooltips: {
callbacks: {
label(tooltipItem, data) {
const label = data.datasets[0].labels[tooltipItem.index] || '',
value = data.datasets[0].data[tooltipItem.index];
const output = ` ${label} : ${value} %`;
return output;
}
},
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
}
},
data2 = {
labels: flightSearch.topData.map(i => i.name),
datasets: [
{
labels: flightSearch.topData.map(i => i.name),
data: flightSearch.topData.map(i => i.value),
// 레드버전
// backgroundColor: [
// '#ffe8d1',
// '#ffb59e',
// '#f0826b',
// '#Bd4f38',
// '#8a1c05'
// ],
backgroundColor: [
'#ccffff',
'#99ffff',
'#66ffff',
'#33efff',
'#00bcd4'
],
borderWidth: 0,
pointStyle: 'rectRounded'
}
]
};
return (
<CustomMainLayout title={titleName}>
<div className='pal-card-box statistics'>
<div>
<Row>
{flight.map((i, idx) => (
<Col md='4' key={idx}>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
<span>
{idx === 0 ? (
<FcAlarmClock />
) : idx === 1 ? (
<FcWorkflow />
) : (
<FcBarChart />
)}
</span>
<span>
{idx === 0
? '총 비행시간'
: idx === 1
? '총 비행거리'
: '총 비행횟수'}
</span>
<span>{i.name}</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>
{idx === 0 ? (
<>{parseTime(i.year)}</>
) : idx === 1 ? (
<>{addCommasToNumber(i.year)}m</>
) : (
<>{addCommasToNumber(i.year)}</>
)}
</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>
{idx === 0 ? (
<>{parseTime(i.month)}</>
) : idx === 1 ? (
<>{addCommasToNumber(i.month)}m</>
) : (
<>{addCommasToNumber(i.month)}</>
)}
</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>
{idx === 0 ? (
<>{parseTime(i.day)}</>
) : idx === 1 ? (
<>{addCommasToNumber(i.day)}m</>
) : (
<>{addCommasToNumber(i.day)}</>
)}
</span>
</td>
</tr>
</table>
</div>
</Col>
))}
</Row>
</div>
<div>
<Row>
<Col>
<div className='mt-2 cont-ti d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row'>
<div>
<h4>검색조건</h4>
</div>
{/* <div className='d-flex align-items-center'>
<Button.Ripple color='primary' size='sm'>
<Search size={16} />
검색
</Button.Ripple>
</div> */}
</div>
<Card>
<CardBody className='pal-card-body'>
<div className='search-cont'>
<dl>
<dt>
<div className='search-box'>
<div className='search-list-ti'>검색조건</div>
<div className='search-list'>
<div className='search-list-cont'>
<Row>
<Col className='list-input' md='4' sm='12'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
onChange={e =>
handleChangeSearchType(
'category',
e.target.value
)
}
>
<option value={'TIME'}>비행 시간</option>
<option value={'DISTANCE'}>
비행 거리
</option>
<option value={'FLT_COUNT'}>
비행 횟수
</option>
</CustomInput>
</Col>
</Row>
</div>
</div>
</div>
</dt>
</dl>
</div>
</CardBody>
</Card>
</Col>
</Row>
</div>
<div>
<Row className='mt-2'>
<Col md='8' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} 통계
</CardTitle>
<div className='select-date-wrap'>
<Row>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.type}
onChange={e =>
handleChangeSearchType('dateType', e.target.value)
}
>
<option value='year'></option>
<option value='month'></option>
<option value='day'></option>
<option value='one-day'>시간</option>
</CustomInput>
</div>
{searchType.dateType === 'month' ||
searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.year}
onChange={e =>
handleChangeSearchType('year', e.target.value)
}
>
<option>2023</option>
</CustomInput>
</div>
{searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.month}
onChange={e =>
handleChangeSearchType(
'month',
e.target.value
)
}
>
{dateLists.month.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
{searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.day}
onChange={e =>
handleChangeSearchType('day', e.target.value)
}
>
{dateLists.day.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
</>
) : null}
</Row>
{/* <Calendar size={14} />
<Flatpickr
options={{
mode: 'range',
defaultDate: ['2019-05-01', '2019-05-10']
}}
className='form-control flat-picker bg-transparent border-0 shadow-none'
/> */}
</div>
</CardHeader>
<CardBody>
<div style={{ height: '400px' }}>
<Bar
data={data}
options={options}
height={400}
plugins={{
afterDraw: function (chart) {
if (data.length <= 0) {
let ctx = chart.chart.ctx;
let width = chart.chart.width;
let height = chart.chart.height;
chart.clear();
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
'표시할 데이터가 없습니다.',
width / 2,
height / 2
);
ctx.restore();
}
}
}}
/>
</div>
</CardBody>
</Card>
</Col>
<Col md='4' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} TOP5
</CardTitle>
</CardHeader>
<CardBody>
<div style={{ height: '275px' }}>
<Doughnut data={data2} options={options2} height={275} />
</div>
{/* <div className='d-flex justify-content-between mt-3 mb-1'>
<div className='d-flex align-items-center'></div>
</div> */}
</CardBody>
</Card>
</Col>
</Row>
</div>
<StatisticsTotal
titleName={titleName}
totalTitle={totalTitle}
totalData={flight}
parseTime={parseTime}
addCommasToNumber={addCommasToNumber}
/>
<StatisticsSearch
searchData={flightSearch}
searchType={searchType}
categoryTypeOptions={categoryTypeOptions}
dateLists={dateLists}
handlerBarTicks={handlerBarTicks}
handlerTitleName={handlerTitleName}
handleChangeSearchType={handleChangeSearchType}
/>
</div>
</CustomMainLayout>
);

455
src/containers/statistics/FlightResultContainer.js

@ -1,26 +1,11 @@
import { CustomMainLayout } from '../../components/layout/CustomMainLayout';
import {
Button,
Col,
Row,
Card,
CardHeader,
CardTitle,
CardBody,
CustomInput
} from 'reactstrap';
import { Search } from 'react-feather';
import { FcAlarmClock, FcWorkflow, FcBarChart } from 'react-icons/fc';
import { Bar, Doughnut } from 'react-chartjs-2';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as StcsActions from '../../modules/statistics/actions';
import StatisticsTotal from '../../components/statistics/StatisticsTotal';
import StatisticsSearch from '../../components/statistics/StatisticsSearch';
export default function ResultContainer({
tooltipShadow,
gridLineColor,
labelColor
}) {
export default function ResultContainer() {
const dispatch = useDispatch();
const { result, resultSearch } = useSelector(state => state.statisticsState);
@ -37,6 +22,12 @@ export default function ResultContainer({
});
const titleName = '비행 실적 통계';
const totalTitle = ['비행 실적', '비행 계획', '비행 승인'];
const categoryTypeOptions = {
FLT_RESULT: '비행 실적',
FLT_PLAN: '비행 계획',
FLT_PLAN_APRVN: '비행 승인'
};
useEffect(() => {
dispatch(StcsActions.RESULT_STCS.request());
@ -83,12 +74,7 @@ export default function ResultContainer({
// 그래프 타이틀 handler
const handlerTitleName = category => {
const categoryMappings = {
FLT_RESULT: '비행 실적',
FLT_PLAN: '비행 계획',
FLT_PLAN_APRVN: '비행 승인'
};
return categoryMappings[category];
return categoryTypeOptions[category];
};
// Bar Graph handler
@ -105,418 +91,29 @@ export default function ResultContainer({
return 10 ** (exponent - 1);
};
// 123456789 -> 123,456,789
const addCommasToNumber = number => {
if (number === 'NoData') return 0;
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const options = {
elements: {
rectangle: {
borderWidth: 2,
borderSkipped: 'bottom'
}
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 500,
legend: {
display: false
},
tooltips: {
// Updated default tooltip UI
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
},
scales: {
xAxes: [
{
display: true,
gridLines: {
display: true,
color: gridLineColor,
zeroLineColor: gridLineColor
},
scaleLabel: {
display: false
},
ticks: {
fontColor: labelColor
}
}
],
yAxes: [
{
display: true,
gridLines: {
color: gridLineColor,
zeroLineColor: gridLineColor
},
ticks: {
...handlerBarTicks(),
min: 0,
fontColor: labelColor
}
}
]
}
},
data = {
labels: resultSearch.graphData.map(i => i.name),
datasets: [
{
data: resultSearch.graphData.map(i => i.value),
backgroundColor: '#00bcd4',
borderColor: '#00bcd4',
barThickness: 15
}
]
};
const options2 = {
responsive: true,
maintainAspectRatio: true,
responsiveAnimationDuration: 500,
cutoutPercentage: 60,
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
padding: 18,
boxWidth: 8,
fontColor: labelColor,
fontSize: 14,
fontWeight: 500,
fontFamily: ['Rubik', 'Montserrat', 'NotoSansKR']
}
},
tooltips: {
callbacks: {
label(tooltipItem, data) {
const label = data.datasets[0].labels[tooltipItem.index] || '',
value = data.datasets[0].data[tooltipItem.index];
const output = ` ${label} : ${value}`;
return output;
}
},
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
}
},
data2 = {
labels: resultSearch.topData.map(i => i.name),
datasets: [
{
labels: resultSearch.topData.map(i => i.name),
data: resultSearch.topData.map(i => i.value),
// 레드버전
// backgroundColor: [
// '#ffe8d1',
// '#ffb59e',
// '#f0826b',
// '#Bd4f38',
// '#8a1c05'
// ],
backgroundColor: [
'#ccffff',
'#99ffff',
'#66ffff',
'#33efff',
'#00bcd4'
],
borderWidth: 0,
pointStyle: 'rectRounded'
}
]
};
return (
<CustomMainLayout title={titleName}>
<div className='pal-card-box statistics'>
<div>
<Row>
{result.map((i, idx) => (
<Col md='4' key={idx}>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
{idx === 0 ? (
<>
<span>
<FcAlarmClock />
</span>
<span>비행 실적</span>
</>
) : idx === 1 ? (
<>
<span>
<FcWorkflow />
</span>
<span>비행 계획</span>
</>
) : (
<>
<span>
<FcBarChart />
</span>
<span>비행 승인</span>
</>
)}
<span>{i.name}</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>
{addCommasToNumber(i.year)}
</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>
{addCommasToNumber(i.month)}
</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>
{addCommasToNumber(i.day)}
</span>
</td>
</tr>
</table>
</div>
</Col>
))}
</Row>
</div>
<div>
<Row>
<Col>
<div className='mt-2 cont-ti d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row'>
<div>
<h4>검색조건</h4>
</div>
{/* <div className='d-flex align-items-center'>
<Button.Ripple color='primary' size='sm'>
<Search size={16} />
검색
</Button.Ripple>
</div> */}
</div>
<Card>
<CardBody className='pal-card-body'>
<div className='search-cont'>
<dl>
<dt>
<div className='search-box'>
<div className='search-list-ti'>검색조건</div>
<div className='search-list'>
<div className='search-list-cont'>
<Row>
<Col className='list-input' md='4' sm='12'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
onChange={e =>
handleChangeSearchType(
'category',
e.target.value
)
}
>
<option value={'FLT_RESULT'}>
비행 실적
</option>
<option value={'FLT_PLAN'}>
비행 계획
</option>
<option value={'FLT_PLAN_APRVN'}>
비행 승인
</option>
</CustomInput>
</Col>
</Row>
</div>
</div>
</div>
</dt>
</dl>
</div>
</CardBody>
</Card>
</Col>
</Row>
</div>
<div>
<Row className='mt-2'>
<Col md='8' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} 통계
</CardTitle>
<div className='select-date-wrap'>
<Row>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.type}
onChange={e =>
handleChangeSearchType('dateType', e.target.value)
}
>
<option value='year'></option>
<option value='month'></option>
<option value='day'></option>
<option value='one-day'>시간</option>
</CustomInput>
</div>
{searchType.dateType === 'month' ||
searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.year}
onChange={e =>
handleChangeSearchType('year', e.target.value)
}
>
<option>2023</option>
</CustomInput>
</div>
{searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.month}
onChange={e =>
handleChangeSearchType(
'month',
e.target.value
)
}
>
{dateLists.month.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
{searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.day}
onChange={e =>
handleChangeSearchType('day', e.target.value)
}
>
{dateLists.day.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
</>
) : null}
</Row>
{/* <Calendar size={14} />
<Flatpickr
options={{
mode: 'range',
defaultDate: ['2019-05-01', '2019-05-10']
}}
className='form-control flat-picker bg-transparent border-0 shadow-none'
/> */}
</div>
</CardHeader>
<CardBody>
<div style={{ height: '400px' }}>
<Bar
data={data}
options={options}
height={400}
plugins={{
afterDraw: function (chart) {
if (data.length <= 0) {
let ctx = chart.chart.ctx;
let width = chart.chart.width;
let height = chart.chart.height;
chart.clear();
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
'표시할 데이터가 없습니다.',
width / 2,
height / 2
);
ctx.restore();
}
}
}}
/>
</div>
</CardBody>
</Card>
</Col>
<Col md='4' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} TOP5
</CardTitle>
</CardHeader>
<CardBody>
<div style={{ height: '275px' }}>
<Doughnut data={data2} options={options2} height={275} />
</div>
{/* <div className='d-flex justify-content-between mt-3 mb-1'>
<div className='d-flex align-items-center'></div>
</div> */}
</CardBody>
</Card>
</Col>
</Row>
</div>
<StatisticsTotal
totalTitle={totalTitle}
totalData={result}
addCommasToNumber={addCommasToNumber}
/>
<StatisticsSearch
searchData={resultSearch}
searchType={searchType}
categoryTypeOptions={categoryTypeOptions}
dateLists={dateLists}
handlerBarTicks={handlerBarTicks}
handlerTitleName={handlerTitleName}
handleChangeSearchType={handleChangeSearchType}
/>
</div>
</CustomMainLayout>
);

Loading…
Cancel
Save