1- import React , { useState , useEffect } from 'react' ;
1+ import React , { useState , useEffect , useMemo } from 'react' ;
22import { connect , useDispatch , useSelector } from 'react-redux' ;
33import {
4- BarChart ,
5- Bar ,
4+ LineChart ,
5+ Line ,
66 XAxis ,
77 YAxis ,
88 CartesianGrid ,
@@ -12,37 +12,309 @@ import {
1212 ResponsiveContainer ,
1313} from 'recharts' ;
1414import { Select , DatePicker , Spin } from 'antd' ;
15+ import dayjs from 'dayjs' ;
1516
16- import axios from 'axios' ;
1717import { fetchInjuriesOverTime } from '../../../actions/bmdashboard/injuryActions' ;
18+ import styles from './InjuriesOverTimeChart.module.css' ;
1819
19- function InjuriesOverTimeChart ( props ) {
20+ const { Option } = Select ;
21+ const { RangePicker } = DatePicker ;
22+
23+ const MONTHS = [
24+ 'January' ,
25+ 'February' ,
26+ 'March' ,
27+ 'April' ,
28+ 'May' ,
29+ 'June' ,
30+ 'July' ,
31+ 'August' ,
32+ 'September' ,
33+ 'October' ,
34+ 'November' ,
35+ 'December' ,
36+ ] ;
37+
38+ const shortId = id => ( id ? String ( id ) . slice ( - 6 ) : 'unknown' ) ;
39+
40+ const generateColors = n =>
41+ Array . from ( { length : n } , ( _ , i ) => `hsl(${ Math . round ( ( 360 / Math . max ( n , 1 ) ) * i ) } ,60%,55%)` ) ;
42+
43+ function CustomTooltip ( { active, payload, label, darkMode } ) {
44+ if ( ! active || ! payload || payload . length === 0 ) return null ;
45+ return (
46+ < div className = { `${ styles . tooltip } ${ darkMode ? styles . tooltipDark : '' } ` } >
47+ < div style = { { fontWeight : 600 , marginBottom : 6 } } > { label } </ div >
48+ { payload
49+ . filter ( p => p . value != null && Number ( p . value ) > 0 )
50+ . map ( p => (
51+ < div key = { p . dataKey } style = { { display : 'flex' , gap : 8 , alignItems : 'center' } } >
52+ < span
53+ style = { {
54+ width : 10 ,
55+ height : 10 ,
56+ background : p . color ,
57+ display : 'inline-block' ,
58+ borderRadius : 2 ,
59+ } }
60+ />
61+ < span > { p . name } </ span >
62+ < span style = { { marginLeft : 'auto' , fontWeight : 600 } } > { p . value } </ span >
63+ </ div >
64+ ) ) }
65+ </ div >
66+ ) ;
67+ }
68+
69+ function InjuriesOverTimeLine ( { darkMode = false } ) {
2070 const dispatch = useDispatch ( ) ;
71+
72+ const rawData = useSelector ( state => state . bmInjury ?. injuryOverTimeData || [ ] ) ;
73+
2174 const [ loading , setLoading ] = useState ( false ) ;
2275
2376 const [ selProjects , setSelProjects ] = useState ( [ ] ) ;
2477 const [ dateRange , setDateRange ] = useState ( [ null , null ] ) ;
2578 const [ selInjTypes , setSelInjTypes ] = useState ( [ ] ) ;
2679 const [ selDepts , setSelDepts ] = useState ( [ ] ) ;
27- const [ selSeverties , setSelSeverities ] = useState ( [ ] ) ;
80+ const [ selSeverities , setSelSeverities ] = useState ( [ ] ) ;
2881
2982 useEffect ( ( ) => {
83+ setLoading ( true ) ;
3084 dispatch (
3185 fetchInjuriesOverTime ( {
3286 projectId : selProjects ,
3387 date : dateRange ,
3488 injuryType : selInjTypes ,
3589 department : selDepts ,
36- severity : selSeverties ,
90+ severity : selSeverities ,
3791 } ) ,
38- ) ;
39- } , [ ] ) ;
92+ ) . finally ( ( ) => setLoading ( false ) ) ;
93+ } , [ dispatch , selProjects , selInjTypes , selDepts , dateRange , selSeverities ] ) ;
94+
95+ const allProjects = useMemo ( ( ) => {
96+ const ids = Array . from ( new Set ( rawData . map ( r => String ( r . projectId ) ) . filter ( Boolean ) ) ) ;
97+ return ids . map ( id => ( { id, label : `Project …${ shortId ( id ) } ` } ) ) ;
98+ } , [ rawData ] ) ;
99+
100+ const allDepartments = useMemo (
101+ ( ) => Array . from ( new Set ( rawData . map ( r => r . department ) . filter ( Boolean ) ) ) ,
102+ [ rawData ] ,
103+ ) ;
104+
105+ const allInjuryTypes = useMemo (
106+ ( ) => Array . from ( new Set ( rawData . map ( r => r . injuryType ) . filter ( Boolean ) ) ) ,
107+ [ rawData ] ,
108+ ) ;
109+
110+ const allSeverities = useMemo (
111+ ( ) => Array . from ( new Set ( rawData . map ( r => r . severity ) . filter ( Boolean ) ) ) ,
112+ [ rawData ] ,
113+ ) ;
114+
115+ const filtered = useMemo ( ( ) => {
116+ const [ start , end ] = dateRange || [ null , null ] ;
117+ return rawData . filter ( r => {
118+ const pid = String ( r . projectId ) ;
119+ const keepProject = selProjects . length === 0 || selProjects . includes ( pid ) ;
120+ const keepDept = selDepts . length === 0 || selDepts . includes ( r . department ) ;
121+ const keepType = selInjTypes . length === 0 || selInjTypes . includes ( r . injuryType ) ;
122+ const keepSev = selSeverities . length === 0 || selSeverities . includes ( r . severity ) ;
123+ if ( ! r . date ) return false ;
124+
125+ let keepDate = true ;
126+ const d = dayjs ( r . date ) ;
127+ if ( start && ! end ) keepDate = d . isSame ( start , 'day' ) || d . isAfter ( start ) ;
128+ if ( ! start && end ) keepDate = d . isSame ( end , 'day' ) || d . isBefore ( end ) ;
129+ if ( start && end )
130+ keepDate =
131+ ( d . isSame ( start , 'day' ) || d . isAfter ( start ) ) && ( d . isSame ( end , 'day' ) || d . isBefore ( end ) ) ;
132+
133+ return keepProject && keepDept && keepType && keepSev && keepDate ;
134+ } ) ;
135+ } , [ rawData , selProjects , selDepts , selInjTypes , selSeverities , dateRange ] ) ;
136+
137+ const visibleProjectIds = useMemo (
138+ ( ) => Array . from ( new Set ( filtered . map ( r => String ( r . projectId ) ) ) ) ,
139+ [ filtered ] ,
140+ ) ;
141+ const visibleProjects = useMemo (
142+ ( ) => visibleProjectIds . map ( id => ( { id, label : `Project …${ shortId ( id ) } ` } ) ) ,
143+ [ visibleProjectIds ] ,
144+ ) ;
145+
146+ const chartData = useMemo ( ( ) => {
147+ const totals = new Map ( ) ;
148+ filtered . forEach ( r => {
149+ const monthIdx = dayjs ( r . date ) . month ( ) ;
150+ const pid = String ( r . projectId ) ;
151+ const key = `${ monthIdx } |${ pid } ` ;
152+ totals . set ( key , ( totals . get ( key ) || 0 ) + ( Number ( r . count ) || 0 ) ) ;
153+ } ) ;
154+
155+ const rows = MONTHS . map ( ( month , idx ) => {
156+ const row = { month } ;
157+ visibleProjects . forEach ( ( { id } ) => {
158+ const v = totals . get ( `${ idx } |${ id } ` ) || 0 ;
159+ row [ id ] = v > 0 ? v : null ;
160+ } ) ;
161+ return row ;
162+ } ) ;
163+
164+ return rows ;
165+ } , [ filtered , visibleProjects ] ) ;
166+
167+ const lineColors = useMemo ( ( ) => generateColors ( visibleProjects . length || 1 ) , [
168+ visibleProjects . length ,
169+ ] ) ;
170+
171+ const maxY = Math . max ( ...chartData . flatMap ( row => visibleProjects . map ( p => row [ p . id ] || 0 ) ) , 0 ) ;
172+
173+ const step = Math . ceil ( maxY / 5 ) ;
174+ const ticks = Array . from ( { length : 6 } , ( _ , i ) => i * step ) ;
40175
41176 return (
42- < >
43- < > InjuriesOverTimeChart</ >
44- </ >
177+ < div className = { `${ styles . wrapper } ${ darkMode ? styles . wrapperDark : '' } ` } >
178+ < h2 className = { styles . title } > Injuries over time</ h2 >
179+
180+ < div className = { styles . filters } >
181+ < Select
182+ className = { styles . filterSelect }
183+ mode = "multiple"
184+ allowClear
185+ placeholder = "Projects"
186+ value = { selProjects }
187+ onChange = { setSelProjects }
188+ maxTagCount = "responsive"
189+ maxTagPlaceholder = { o => `+${ o . length } ` }
190+ >
191+ { allProjects . map ( p => (
192+ < Option key = { p . id } value = { p . id } >
193+ { p . label }
194+ </ Option >
195+ ) ) }
196+ </ Select >
197+
198+ < RangePicker
199+ className = { styles . filterSelect }
200+ value = { dateRange }
201+ onChange = { dates => setDateRange ( dates || [ null , null ] ) }
202+ />
203+
204+ < Select
205+ className = { styles . filterSelect }
206+ mode = "multiple"
207+ allowClear
208+ placeholder = "Injury Types"
209+ value = { selInjTypes }
210+ onChange = { setSelInjTypes }
211+ maxTagCount = "responsive"
212+ maxTagPlaceholder = { o => `+${ o . length } ` }
213+ >
214+ { allInjuryTypes . map ( t => (
215+ < Option key = { t } value = { t } >
216+ { t }
217+ </ Option >
218+ ) ) }
219+ </ Select >
220+
221+ < Select
222+ className = { styles . filterSelect }
223+ mode = "multiple"
224+ allowClear
225+ placeholder = "Departments"
226+ value = { selDepts }
227+ onChange = { setSelDepts }
228+ maxTagCount = "responsive"
229+ maxTagPlaceholder = { o => `+${ o . length } ` }
230+ >
231+ { allDepartments . map ( d => (
232+ < Option key = { d } value = { d } >
233+ { d }
234+ </ Option >
235+ ) ) }
236+ </ Select >
237+
238+ < Select
239+ className = { styles . filterSelect }
240+ mode = "multiple"
241+ allowClear
242+ placeholder = "Severity"
243+ value = { selSeverities }
244+ onChange = { setSelSeverities }
245+ maxTagCount = "responsive"
246+ maxTagPlaceholder = { o => `+${ o . length } ` }
247+ >
248+ { allSeverities . map ( s => (
249+ < Option key = { s } value = { s } >
250+ { s }
251+ </ Option >
252+ ) ) }
253+ </ Select >
254+ </ div >
255+
256+ { loading ? (
257+ < div style = { { textAlign : 'center' , padding : 50 } } >
258+ < Spin size = "large" />
259+ </ div >
260+ ) : visibleProjects ?. length > 0 ? (
261+ < div className = { `${ styles . chartCard } ${ darkMode ? styles . chartCardDark : '' } ` } >
262+ < ResponsiveContainer width = "100%" height = "100%" >
263+ < LineChart data = { chartData } margin = { { top : 20 , right : 30 , left : 20 , bottom : 5 } } >
264+ < CartesianGrid strokeDasharray = "3 3" strokeOpacity = { darkMode ? 0.2 : 1 } />
265+ < XAxis dataKey = "month" height = { 60 } angle = { - 25 } textAnchor = "end" interval = { 0 } />
266+ < YAxis
267+ label = { { value : 'Injury Count' , angle : - 90 , position : 'insideLeft' } }
268+ allowDecimals = { false }
269+ domain = { [ 0 , maxY ] }
270+ ticks = { ticks }
271+ />
272+ < Tooltip content = { < CustomTooltip darkMode = { darkMode } /> } />
273+ < Legend verticalAlign = "top" align = "left" wrapperStyle = { { paddingBottom : 20 } } />
274+ { visibleProjects . map ( ( proj , idx ) => (
275+ < Line
276+ key = { proj . id }
277+ type = "linear"
278+ dataKey = { proj . id }
279+ name = { proj . label }
280+ stroke = { lineColors [ idx ] }
281+ dot = { { r : 3 } }
282+ activeDot = { { r : 5 } }
283+ connectNulls
284+ >
285+ < LabelList
286+ position = "top"
287+ content = { props => {
288+ const { x, y, value } = props ;
289+ if ( ! value || value <= 0 ) return null ;
290+ return (
291+ < text
292+ x = { x }
293+ y = { y - 6 }
294+ fill = { lineColors [ idx ] }
295+ textAnchor = "middle"
296+ fontSize = { 12 }
297+ fontWeight = "bold"
298+ >
299+ { value }
300+ </ text >
301+ ) ;
302+ } }
303+ />
304+ </ Line >
305+ ) ) }
306+ </ LineChart >
307+ </ ResponsiveContainer >
308+ </ div >
309+ ) : (
310+ < p className = { `${ styles . noData } ${ darkMode ? styles . noDataDark : '' } ` } > No Data Available</ p >
311+ ) }
312+ </ div >
45313 ) ;
46314}
47315
48- export default InjuriesOverTimeChart ;
316+ const mapStateToProps = state => ( {
317+ darkMode : state ?. theme ?. darkMode ,
318+ } ) ;
319+
320+ export default connect ( mapStateToProps ) ( InjuriesOverTimeLine ) ;
0 commit comments