datastake-daf 0.6.767 → 0.6.769
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/index.js +1007 -730
- package/dist/layouts/index.js +495 -459
- package/dist/pages/index.js +7914 -6836
- package/dist/style/datastake/mapbox-gl.css +330 -0
- package/dist/utils/index.js +481 -457
- package/package.json +1 -1
- package/src/@daf/core/components/Charts/ColumnChart/index.jsx +10 -0
- package/src/@daf/core/components/Charts/LineChart/index.jsx +14 -0
- package/src/@daf/core/components/Dashboard/Map/ChainIcon/Markers/StakeholderMarker.js +5 -2
- package/src/@daf/core/components/Dashboard/Map/ChainIcon/index.js +67 -27
- package/src/@daf/core/components/Dashboard/Map/hook.js +26 -32
- package/src/@daf/core/components/Dashboard/Widget/ActivityIndicators/index.jsx +2 -0
- package/src/@daf/core/components/Dashboard/Widget/StatCard/StatCard.stories.js +226 -0
- package/src/@daf/core/components/Dashboard/Widget/StatCard/index.js +103 -0
- package/src/@daf/core/components/Dashboard/Widget/StatCard/style.js +83 -0
- package/src/@daf/core/components/Icon/configs/Down.js +8 -0
- package/src/@daf/core/components/Icon/configs/Up.js +8 -0
- package/src/@daf/core/components/Icon/configs/index.js +4 -0
- package/src/@daf/core/components/Icon/configs/partnerIcon.js +1 -1
- package/src/@daf/core/components/Screens/BaseScreen/index.jsx +1 -1
- package/src/@daf/core/components/Screens/TableScreen/TablePageWithTabs/index.jsx +1 -1
- package/src/@daf/core/components/Sidenav/Menu.jsx +4 -4
- package/src/@daf/core/components/UI/MissingTagButton/index.jsx +36 -0
- package/src/@daf/pages/Dashboards/SupplyChain/components/SupplyChainMap/index.js +0 -2
- package/src/@daf/pages/Documents/config.js +0 -10
- package/src/@daf/pages/Documents/index.jsx +51 -108
- package/src/@daf/pages/Events/Activities/config.js +1 -11
- package/src/@daf/pages/Events/Activities/index.jsx +47 -105
- package/src/@daf/pages/Events/Incidents/config.js +1 -11
- package/src/@daf/pages/Events/Incidents/index.jsx +47 -105
- package/src/@daf/pages/Events/config.js +18 -34
- package/src/@daf/pages/Events/index.jsx +49 -111
- package/src/@daf/pages/Locations/MineSite/config.js +0 -10
- package/src/@daf/pages/Locations/MineSite/index.jsx +47 -105
- package/src/@daf/pages/Locations/config.js +4 -16
- package/src/@daf/pages/Locations/index.jsx +53 -110
- package/src/@daf/pages/Stakeholders/Operators/config.js +0 -10
- package/src/@daf/pages/Stakeholders/Operators/index.jsx +47 -105
- package/src/@daf/pages/Stakeholders/Workers/config.js +0 -10
- package/src/@daf/pages/Stakeholders/Workers/index.jsx +47 -105
- package/src/@daf/pages/Stakeholders/config.js +3 -15
- package/src/@daf/pages/Stakeholders/index.jsx +53 -109
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx +43 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/CommunityStats/helper.js +60 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/CommunityStats/index.jsx +36 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/GenderDistribution/helper.js +117 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/GenderDistribution/index.jsx +49 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/JobsTimeline/index.jsx +212 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/index.jsx +72 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/CyclePartners/helper.js +91 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/CyclePartners/index.jsx +50 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/helper.js +134 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/index.jsx +49 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/index.jsx +112 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/index.jsx +498 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/KeyInformation/index.jsx +49 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/PlantingLocations/index.jsx +120 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/config.js +5 -10
- package/src/@daf/pages/Summary/Activities/PlantingCycle/helper.js +218 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/index.jsx +22 -32
- package/src/@daf/pages/Summary/Activities/Restoration/components/ActivityImagery/index.jsx +29 -0
- package/src/@daf/pages/Summary/Activities/Restoration/components/ActivityLocation/index.jsx +94 -0
- package/src/@daf/pages/Summary/Activities/Restoration/components/WorkersDistribution/index.jsx +49 -0
- package/src/@daf/pages/Summary/Activities/Restoration/index.jsx +16 -138
- package/src/@daf/pages/TablePage/config.js +78 -0
- package/src/@daf/{core/components/Screens/TableScreen/TableWithTabsAndCreate → pages/TablePage}/create.jsx +6 -5
- package/src/@daf/pages/TablePage/hook.js +123 -0
- package/src/@daf/pages/TablePage/index.jsx +142 -0
- package/src/index.js +2 -0
- package/src/@daf/core/components/Screens/TableScreen/TableWithTabsAndCreate/index.jsx +0 -115
- package/src/@daf/pages/Documents/create.jsx +0 -105
- package/src/@daf/pages/Events/Activities/create.jsx +0 -104
- package/src/@daf/pages/Events/Incidents/create.jsx +0 -104
- package/src/@daf/pages/Events/create.jsx +0 -104
- package/src/@daf/pages/Locations/MineSite/create.jsx +0 -104
- package/src/@daf/pages/Locations/create.jsx +0 -104
- package/src/@daf/pages/Stakeholders/Operators/create.jsx +0 -104
- package/src/@daf/pages/Stakeholders/Workers/create.jsx +0 -104
- package/src/@daf/pages/Stakeholders/create.jsx +0 -105
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { Widget, ColumnChart } from '../../../../../../../../index.js';
|
|
4
|
+
|
|
5
|
+
const JobsTimeline = ({
|
|
6
|
+
dayJobsTimeline,
|
|
7
|
+
loading = false,
|
|
8
|
+
t = (s) => s
|
|
9
|
+
}) => {
|
|
10
|
+
|
|
11
|
+
console.log('dayJobsTimeline', dayJobsTimeline);
|
|
12
|
+
const jobsData = Array.isArray(dayJobsTimeline)
|
|
13
|
+
? dayJobsTimeline
|
|
14
|
+
: (dayJobsTimeline?.jobsTimeline || dayJobsTimeline?.jobs || dayJobsTimeline?.timeline || []);
|
|
15
|
+
|
|
16
|
+
const formatDateAxis = useMemo(() => {
|
|
17
|
+
return (label) => {
|
|
18
|
+
if (!label) return label;
|
|
19
|
+
|
|
20
|
+
// Try to parse the date using dayjs with various formats
|
|
21
|
+
let date = dayjs(label);
|
|
22
|
+
|
|
23
|
+
// If first attempt fails, try parsing as ISO date string
|
|
24
|
+
if (!date.isValid() && typeof label === 'string') {
|
|
25
|
+
date = dayjs(label, ['YYYY-MM-DD', 'YYYY-MM', 'MMM YY', 'MMM YYYY'], true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If it's a valid date, format it as "Mmm YY"
|
|
29
|
+
if (date.isValid()) {
|
|
30
|
+
return date.format('MMM YY');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// If it's already in "Mmm YY" format or similar, return as is
|
|
34
|
+
// Otherwise return the original label
|
|
35
|
+
return label;
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const jobsTimelineData = useMemo(() => {
|
|
40
|
+
// Always show last 12 months, even if no data
|
|
41
|
+
const now = dayjs().startOf('month');
|
|
42
|
+
const twelveMonthsAgo = now.subtract(11, 'month'); // 11 months ago + current month = 12 months
|
|
43
|
+
|
|
44
|
+
// Create a map of existing data by month (YYYY-MM format)
|
|
45
|
+
const dataMap = new Map();
|
|
46
|
+
const dates = [];
|
|
47
|
+
|
|
48
|
+
// Process jobs data if available
|
|
49
|
+
if (jobsData && Array.isArray(jobsData) && jobsData.length > 0) {
|
|
50
|
+
jobsData.forEach((item) => {
|
|
51
|
+
if (typeof item === 'object' && item !== null && item.date) {
|
|
52
|
+
const date = dayjs(item.date);
|
|
53
|
+
if (date.isValid()) {
|
|
54
|
+
const monthKey = date.format('YYYY-MM');
|
|
55
|
+
const count = Number(item.total || item.count || item.jobs || item.value || 0) || 0;
|
|
56
|
+
dates.push(date);
|
|
57
|
+
|
|
58
|
+
// If multiple entries for same month, sum them
|
|
59
|
+
if (dataMap.has(monthKey)) {
|
|
60
|
+
dataMap.set(monthKey, {
|
|
61
|
+
...dataMap.get(monthKey),
|
|
62
|
+
jobs: dataMap.get(monthKey).jobs + count,
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
dataMap.set(monthKey, {
|
|
66
|
+
month: item.date,
|
|
67
|
+
jobs: count,
|
|
68
|
+
date: item.date,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Determine date range
|
|
77
|
+
let minDate = twelveMonthsAgo;
|
|
78
|
+
let maxDate = now;
|
|
79
|
+
|
|
80
|
+
// If we have data, adjust range to include it
|
|
81
|
+
if (dates.length > 0) {
|
|
82
|
+
const sortedDates = dates.sort((a, b) => a.valueOf() - b.valueOf());
|
|
83
|
+
const firstDataDate = sortedDates[0].startOf('month');
|
|
84
|
+
const lastDataDate = sortedDates[sortedDates.length - 1].startOf('month');
|
|
85
|
+
|
|
86
|
+
// Start from the earlier of: 12 months ago, or first data date
|
|
87
|
+
minDate = twelveMonthsAgo.isBefore(firstDataDate) ? twelveMonthsAgo : firstDataDate;
|
|
88
|
+
|
|
89
|
+
// End at the later of: current month, or last data date
|
|
90
|
+
maxDate = now.isAfter(lastDataDate) ? now : lastDataDate;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate all months in the range
|
|
94
|
+
const result = [];
|
|
95
|
+
let currentDate = minDate.clone();
|
|
96
|
+
|
|
97
|
+
while (currentDate.isBefore(maxDate) || currentDate.isSame(maxDate, 'month')) {
|
|
98
|
+
const monthKey = currentDate.format('YYYY-MM');
|
|
99
|
+
const existingData = dataMap.get(monthKey);
|
|
100
|
+
|
|
101
|
+
if (existingData) {
|
|
102
|
+
result.push(existingData);
|
|
103
|
+
} else {
|
|
104
|
+
// Fill missing month with 0
|
|
105
|
+
result.push({
|
|
106
|
+
month: currentDate.format('YYYY-MM-DD'), // Use first day of month
|
|
107
|
+
jobs: 0,
|
|
108
|
+
date: currentDate.format('YYYY-MM-DD'),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
currentDate = currentDate.add(1, 'month');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}, [jobsData]);
|
|
117
|
+
|
|
118
|
+
const maxYValue = useMemo(() => {
|
|
119
|
+
if (!jobsTimelineData || jobsTimelineData.length === 0) {
|
|
120
|
+
return 100;
|
|
121
|
+
}
|
|
122
|
+
const maxValue = Math.max(...jobsTimelineData.map(item => item.jobs || 0));
|
|
123
|
+
// If max is 0, set default to 100 to show Y-axis
|
|
124
|
+
// Otherwise, round up to nearest 10
|
|
125
|
+
if (maxValue === 0) {
|
|
126
|
+
return 100;
|
|
127
|
+
}
|
|
128
|
+
return Math.ceil(maxValue / 10) * 10 || 100;
|
|
129
|
+
}, [jobsTimelineData]);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Widget
|
|
133
|
+
title={<div>{t("Day Jobs Timeline")}</div>}
|
|
134
|
+
className="with-border-header h-w-btn-header "
|
|
135
|
+
>
|
|
136
|
+
<ColumnChart
|
|
137
|
+
data={jobsTimelineData}
|
|
138
|
+
xFieldKey="month"
|
|
139
|
+
yFieldKey="jobs"
|
|
140
|
+
animated={true}
|
|
141
|
+
// height={200}
|
|
142
|
+
color="#016C6E"
|
|
143
|
+
renderTooltipContent={(title, data) => {
|
|
144
|
+
if (!data || data.length === 0) return {};
|
|
145
|
+
// For ColumnChart, data structure: data[0]?.data contains the actual data point
|
|
146
|
+
const item = data[0]?.data || data[0];
|
|
147
|
+
const count = item?.jobs || item?.value || 0;
|
|
148
|
+
// Title is the X-axis value (month/date), use it for formatting
|
|
149
|
+
const dateValue = item?.date || title || '';
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
title: t("straatos::local-jobs-created"),
|
|
153
|
+
subTitle: formatDateAxis(dateValue),
|
|
154
|
+
items: [
|
|
155
|
+
{
|
|
156
|
+
label: t("straatos::jobs"),
|
|
157
|
+
value: count,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}}
|
|
162
|
+
formattedXAxis={formatDateAxis}
|
|
163
|
+
formattedYAxis={(value) => {
|
|
164
|
+
return `${value}`.replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => `${s},`);
|
|
165
|
+
}}
|
|
166
|
+
yAxis={{
|
|
167
|
+
min: 0,
|
|
168
|
+
max: maxYValue,
|
|
169
|
+
tickMethod: () => {
|
|
170
|
+
|
|
171
|
+
const step = maxYValue <= 100 ? 5 : Math.max(5, Math.floor(maxYValue / 10));
|
|
172
|
+
const ticks = [];
|
|
173
|
+
for (let i = 0; i <= maxYValue; i += step) {
|
|
174
|
+
ticks.push(i);
|
|
175
|
+
}
|
|
176
|
+
// Ensure max value is included
|
|
177
|
+
if (ticks.length === 0 || ticks[ticks.length - 1] < maxYValue) {
|
|
178
|
+
ticks.push(maxYValue);
|
|
179
|
+
}
|
|
180
|
+
return ticks;
|
|
181
|
+
},
|
|
182
|
+
label: {
|
|
183
|
+
style: {
|
|
184
|
+
fontSize: 12,
|
|
185
|
+
fill: '#666',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
grid: {
|
|
189
|
+
line: {
|
|
190
|
+
style: {
|
|
191
|
+
stroke: '#E5E7EB',
|
|
192
|
+
lineWidth: 1,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}}
|
|
197
|
+
xAxis={{
|
|
198
|
+
label: {
|
|
199
|
+
formatter: formatDateAxis,
|
|
200
|
+
autoHide: true,
|
|
201
|
+
style: {
|
|
202
|
+
fontSize: 12,
|
|
203
|
+
fill: '#666',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
</Widget>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export default JobsTimeline;
|
package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/index.jsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
|
|
3
|
+
import { Widget } from '../../../../../../../../src/index.js';
|
|
4
|
+
import GenderDistribution from './GenderDistribution/index.jsx';
|
|
5
|
+
import JobsTimeline from './JobsTimeline/index.jsx';
|
|
6
|
+
import CommunityStats from './CommunityStats/index.jsx';
|
|
7
|
+
|
|
8
|
+
const CommunityParticipation = ({
|
|
9
|
+
id,
|
|
10
|
+
getSummaryDetail,
|
|
11
|
+
loading = false,
|
|
12
|
+
t = (s) => s
|
|
13
|
+
}) => {
|
|
14
|
+
|
|
15
|
+
const defaultConfig = useMemo(
|
|
16
|
+
() => ({
|
|
17
|
+
basepath: "planting-cycle",
|
|
18
|
+
url: `/summary/${id}/community-participation`,
|
|
19
|
+
stop: !id,
|
|
20
|
+
}),
|
|
21
|
+
[id],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const customGetData = useMemo(() => {
|
|
25
|
+
if (getSummaryDetail && id) {
|
|
26
|
+
return ({ url, params = {} }) => {
|
|
27
|
+
const match = url.match(/\/summary\/[^/]+\/(.+)/);
|
|
28
|
+
if (match) {
|
|
29
|
+
const [, type] = match;
|
|
30
|
+
return getSummaryDetail(id, type, params);
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}, [getSummaryDetail, id]);
|
|
37
|
+
|
|
38
|
+
const { loading: communityParticipationLoading, data: communityParticipationData } = useWidgetFetch({
|
|
39
|
+
config: defaultConfig,
|
|
40
|
+
getData: customGetData
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const { dayJobs, employedWomen, dayJobsTimeline, genderDistribution } = communityParticipationData;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section>
|
|
47
|
+
<Widget
|
|
48
|
+
title={t("Community Participation")}
|
|
49
|
+
loading={loading}
|
|
50
|
+
className="with-border-header h-w-btn-header"
|
|
51
|
+
>
|
|
52
|
+
<CommunityStats
|
|
53
|
+
dayJobs={dayJobs}
|
|
54
|
+
employedWomen={employedWomen}
|
|
55
|
+
t={t}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<div style={{ display: 'flex', gap: '24px' , }}>
|
|
59
|
+
<section style={{ flex: 1 , height: '100%' }}>
|
|
60
|
+
<JobsTimeline dayJobsTimeline={dayJobsTimeline} loading={communityParticipationLoading} t={t} />
|
|
61
|
+
</section>
|
|
62
|
+
<section style={{ flex: 1 , height: '100%' }}>
|
|
63
|
+
<GenderDistribution genderDistribution={genderDistribution} loading={communityParticipationLoading} t={t} style={{ flex: 1 }} />
|
|
64
|
+
</section>
|
|
65
|
+
</div>
|
|
66
|
+
</Widget>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default CommunityParticipation;
|
|
72
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const PARTNER_COLORS = ['#016C6E', '#00AEB1', '#FF7A45', '#52C41A', '#1890FF', '#722ED1', '#FA8C16', '#EB2F96'];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a direct array of partners to distribution format
|
|
5
|
+
* @param {Array} partnersArray - Array of partner objects: [{ name: "Partner 1", count: 5 }, ...]
|
|
6
|
+
* @returns {Object} Distribution object: { "Partner 1": 5, "Partner 2": 3, ... }
|
|
7
|
+
*/
|
|
8
|
+
export const getCyclePartners = (partnersArray) => {
|
|
9
|
+
if (!Array.isArray(partnersArray)) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const distribution = {};
|
|
14
|
+
partnersArray.forEach(partner => {
|
|
15
|
+
const name = partner?.name || partner?.nickName || partner?.label || 'Unknown';
|
|
16
|
+
const count = partner?.count || partner?.value || 1;
|
|
17
|
+
distribution[name] = (distribution[name] || 0) + count;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return distribution;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks if the partners distribution data is empty
|
|
25
|
+
* @param {Object} partnersDistributionData - Distribution object: { "Partner 1": 5, ... }
|
|
26
|
+
* @returns {boolean} True if all values are 0 or empty
|
|
27
|
+
*/
|
|
28
|
+
export const isPartnersDistributionEmpty = (partnersDistributionData) => {
|
|
29
|
+
return Object.values(partnersDistributionData).every(val => !val || val === 0);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Calculates pie chart data from partners distribution
|
|
34
|
+
* @param {Object} partnersDistributionData - Distribution object: { "Partner 1": 5, ... }
|
|
35
|
+
* @returns {Array} Array of pie chart data points with value, percent, color, label, and key
|
|
36
|
+
*/
|
|
37
|
+
export const calculatePartnersPieData = (partnersDistributionData) => {
|
|
38
|
+
const total = Object.values(partnersDistributionData).reduce((all, val) => all + (val || 0), 0);
|
|
39
|
+
|
|
40
|
+
return Object.keys(partnersDistributionData).map((key, index) => {
|
|
41
|
+
const color = PARTNER_COLORS[index % PARTNER_COLORS.length];
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
value: partnersDistributionData[key] || 0,
|
|
45
|
+
percent: total > 0 ? (partnersDistributionData[key] || 0) / total : 0,
|
|
46
|
+
color: color,
|
|
47
|
+
label: key,
|
|
48
|
+
key: key,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates tooltip content for partners pie chart
|
|
55
|
+
* @param {Object} item - The pie chart item being hovered
|
|
56
|
+
* @param {boolean} isEmpty - Whether the distribution is empty
|
|
57
|
+
* @param {Object} partnersDistributionData - Distribution object
|
|
58
|
+
* @param {Function} t - Translation function
|
|
59
|
+
* @param {Function} renderTooltipJsx - Function to render tooltip JSX
|
|
60
|
+
* @returns {JSX.Element|null} Tooltip content or null
|
|
61
|
+
*/
|
|
62
|
+
export const getPartnersTooltipChildren = (item, isEmpty, partnersDistributionData, t, renderTooltipJsx) => {
|
|
63
|
+
if (isEmpty) {
|
|
64
|
+
if (!Object.keys(partnersDistributionData).length) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return renderTooltipJsx({
|
|
69
|
+
title: t("Cycle Partners"),
|
|
70
|
+
items: Object.keys(partnersDistributionData).map((k) => ({
|
|
71
|
+
label: t("Partner"),
|
|
72
|
+
value: k,
|
|
73
|
+
})),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return renderTooltipJsx({
|
|
78
|
+
title: t("Cycle Partners"),
|
|
79
|
+
items: [
|
|
80
|
+
{
|
|
81
|
+
label: t("Partner"),
|
|
82
|
+
value: item.label || item.key || 'Unknown',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: t("Activities"),
|
|
86
|
+
value: `${item.value || 0}`,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useMemo, useCallback } from 'react';
|
|
2
|
+
import { Widget, PieChart } from '../../../../../../../../index.js';
|
|
3
|
+
import { getCyclePartners, isPartnersDistributionEmpty, calculatePartnersPieData, getPartnersTooltipChildren } from './helper';
|
|
4
|
+
import { renderTooltipJsx } from '../../../../../../../../utils.js';
|
|
5
|
+
|
|
6
|
+
const CyclePartners = ({
|
|
7
|
+
cyclePartners,
|
|
8
|
+
loading = false,
|
|
9
|
+
t = (s) => s
|
|
10
|
+
}) => {
|
|
11
|
+
|
|
12
|
+
const partnersDistributionData = useMemo(() => getCyclePartners(cyclePartners), [cyclePartners]);
|
|
13
|
+
const isEmpty = useMemo(() => isPartnersDistributionEmpty(partnersDistributionData), [partnersDistributionData]);
|
|
14
|
+
const pieData = useMemo(() => calculatePartnersPieData(partnersDistributionData), [partnersDistributionData]);
|
|
15
|
+
|
|
16
|
+
const getTooltipChildren = useCallback(
|
|
17
|
+
(item) => getPartnersTooltipChildren(item, isEmpty, partnersDistributionData, t, renderTooltipJsx),
|
|
18
|
+
[t, isEmpty, partnersDistributionData],
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Widget
|
|
23
|
+
loading={loading}
|
|
24
|
+
title={<div>{t("Cycle Partners")}</div>}
|
|
25
|
+
className="with-border-header h-w-btn-header "
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
29
|
+
marginTop: "auto",
|
|
30
|
+
marginBottom: "auto",
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<PieChart
|
|
34
|
+
mouseXOffset={10}
|
|
35
|
+
mouseYOffset={10}
|
|
36
|
+
changeOpacityOnHover={false}
|
|
37
|
+
data={pieData}
|
|
38
|
+
doConstraints={false}
|
|
39
|
+
isPie
|
|
40
|
+
t={t}
|
|
41
|
+
isEmpty={isEmpty}
|
|
42
|
+
getTooltipChildren={getTooltipChildren}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
</Widget>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default CyclePartners;
|
|
50
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const HEALTH_SAFETY_COLORS = {
|
|
2
|
+
compliant: '#52C41A',
|
|
3
|
+
notCompliant: '#FF4D4F',
|
|
4
|
+
empty: '#D9D9D9',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const getIndicatorType = (value) => {
|
|
8
|
+
if (value === "yes" || value === true) return "compliant";
|
|
9
|
+
if (value === "no" || value === false) return "notCompliant";
|
|
10
|
+
if (value === null || value === undefined) return "empty";
|
|
11
|
+
return "empty";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gets health and safety distribution data from activity data
|
|
16
|
+
* @param {Object} activityData - Activity data object
|
|
17
|
+
* @returns {Object} Distribution object with compliant, notCompliant, and empty counts
|
|
18
|
+
*/
|
|
19
|
+
export const getHealthAndSafetyDistributionData = (activityData) => {
|
|
20
|
+
// Define health and safety indicator fields
|
|
21
|
+
const indicators = [
|
|
22
|
+
{ field: 'aidKitAccessible', label: 'Aid kit availability' },
|
|
23
|
+
{ field: 'hsTrainingConfirmation', label: 'H&S training delivery' },
|
|
24
|
+
{ field: 'duosFormed', label: 'Workers safe pairing' },
|
|
25
|
+
{ field: 'presenceOfChildren', label: 'No children' },
|
|
26
|
+
{ field: 'focalPointPresent', label: 'Security presence' },
|
|
27
|
+
{ field: 'relayPresent', label: 'Relay presence' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const distribution = {
|
|
31
|
+
compliant: 0,
|
|
32
|
+
notCompliant: 0,
|
|
33
|
+
empty: 0,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
indicators.forEach(({ field }) => {
|
|
37
|
+
const value = activityData?.[field];
|
|
38
|
+
const type = getIndicatorType(value);
|
|
39
|
+
distribution[type] = (distribution[type] || 0) + 1;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return distribution;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Checks if the health and safety distribution data is empty
|
|
47
|
+
* @param {Object} healthAndSafetyDistributionData - Distribution object
|
|
48
|
+
* @returns {boolean} True if all values are 0 or empty
|
|
49
|
+
*/
|
|
50
|
+
export const isHealthAndSafetyDistributionEmpty = (healthAndSafetyDistributionData) => {
|
|
51
|
+
return Object.values(healthAndSafetyDistributionData).every(val => !val || val === 0);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calculates pie chart data from health and safety distribution
|
|
56
|
+
* @param {Object} healthAndSafetyDistributionData - Distribution object
|
|
57
|
+
* @param {Function} t - Translation function
|
|
58
|
+
* @returns {Array} Array of pie chart data points with value, percent, color, label, and key
|
|
59
|
+
*/
|
|
60
|
+
export const calculateHealthAndSafetyPieData = (healthAndSafetyDistributionData, t) => {
|
|
61
|
+
const total = Object.values(healthAndSafetyDistributionData).reduce((all, val) => all + (val || 0), 0);
|
|
62
|
+
|
|
63
|
+
const labels = {
|
|
64
|
+
compliant: t("Compliant"),
|
|
65
|
+
notCompliant: t("Not Compliant"),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return Object.keys(healthAndSafetyDistributionData).map((key) => {
|
|
69
|
+
const color = HEALTH_SAFETY_COLORS[key] || '#D9D9D9';
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
value: healthAndSafetyDistributionData[key] || 0,
|
|
73
|
+
percent: total > 0 ? (healthAndSafetyDistributionData[key] || 0) / total : 0,
|
|
74
|
+
color: color,
|
|
75
|
+
label: labels[key] || key,
|
|
76
|
+
key: key,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generates tooltip content for health and safety pie chart
|
|
83
|
+
* Shows all statuses with their percentages
|
|
84
|
+
* @param {Object} item - The pie chart item being hovered (not used, but kept for compatibility)
|
|
85
|
+
* @param {boolean} isEmpty - Whether the distribution is empty
|
|
86
|
+
* @param {Object} healthAndSafetyDistributionData - Distribution object
|
|
87
|
+
* @param {Function} t - Translation function
|
|
88
|
+
* @param {Function} renderTooltipJsx - Function to render tooltip JSX
|
|
89
|
+
* @returns {JSX.Element|null} Tooltip content or null
|
|
90
|
+
*/
|
|
91
|
+
export const getHealthAndSafetyTooltipChildren = (item, isEmpty, healthAndSafetyDistributionData, t, renderTooltipJsx) => {
|
|
92
|
+
// If empty or no data, return null to display nothing
|
|
93
|
+
if (isEmpty || !Object.keys(healthAndSafetyDistributionData).length) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate total for percentage calculation
|
|
98
|
+
const total = Object.values(healthAndSafetyDistributionData).reduce((all, val) => all + (val || 0), 0);
|
|
99
|
+
|
|
100
|
+
// If total is 0, return null to display nothing
|
|
101
|
+
if (total === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const labels = {
|
|
106
|
+
compliant: t("Compliant"),
|
|
107
|
+
notCompliant: t("Not Compliant"),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Filter items with values > 0
|
|
111
|
+
const itemsWithData = Object.keys(healthAndSafetyDistributionData)
|
|
112
|
+
.filter(key => healthAndSafetyDistributionData[key] > 0)
|
|
113
|
+
.map((key) => {
|
|
114
|
+
const value = healthAndSafetyDistributionData[key] || 0;
|
|
115
|
+
const percent = total > 0 ? ((value / total) * 100).toFixed(0) : 0;
|
|
116
|
+
return {
|
|
117
|
+
color: HEALTH_SAFETY_COLORS[key] || '#D9D9D9',
|
|
118
|
+
label: labels[key] || key,
|
|
119
|
+
value: `${percent}%`,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// If no items with data, return null
|
|
124
|
+
if (itemsWithData.length === 0) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Show all items with their percentages
|
|
129
|
+
return renderTooltipJsx({
|
|
130
|
+
title: t("Health and Safety"),
|
|
131
|
+
items: itemsWithData,
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useMemo, useCallback } from 'react';
|
|
2
|
+
import { Widget, PieChart } from '../../../../../../../../index.js';
|
|
3
|
+
import { getHealthAndSafetyDistributionData, isHealthAndSafetyDistributionEmpty, calculateHealthAndSafetyPieData, getHealthAndSafetyTooltipChildren } from './helper';
|
|
4
|
+
import { renderTooltipJsx } from '../../../../../../../../utils';
|
|
5
|
+
|
|
6
|
+
const HealthAndSafety = ({
|
|
7
|
+
activityData,
|
|
8
|
+
loading = false,
|
|
9
|
+
t = (s) => s
|
|
10
|
+
}) => {
|
|
11
|
+
const healthAndSafetyDistributionData = useMemo(() => getHealthAndSafetyDistributionData(activityData), [activityData]);
|
|
12
|
+
const isEmpty = useMemo(() => isHealthAndSafetyDistributionEmpty(healthAndSafetyDistributionData), [healthAndSafetyDistributionData]);
|
|
13
|
+
const pieData = useMemo(() => calculateHealthAndSafetyPieData(healthAndSafetyDistributionData, t), [healthAndSafetyDistributionData, t]);
|
|
14
|
+
|
|
15
|
+
const getTooltipChildren = useCallback(
|
|
16
|
+
(item) => getHealthAndSafetyTooltipChildren(item, isEmpty, healthAndSafetyDistributionData, t, renderTooltipJsx),
|
|
17
|
+
[t, isEmpty, healthAndSafetyDistributionData],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Widget
|
|
22
|
+
loading={loading}
|
|
23
|
+
title={<div>{t("Health and Safety")}</div>}
|
|
24
|
+
className="with-border-header h-w-btn-header "
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
style={{
|
|
28
|
+
marginTop: "auto",
|
|
29
|
+
marginBottom: "auto",
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<PieChart
|
|
33
|
+
mouseXOffset={10}
|
|
34
|
+
mouseYOffset={10}
|
|
35
|
+
changeOpacityOnHover={false}
|
|
36
|
+
data={pieData}
|
|
37
|
+
doConstraints={false}
|
|
38
|
+
isPie
|
|
39
|
+
t={t}
|
|
40
|
+
isEmpty={isEmpty}
|
|
41
|
+
getTooltipChildren={getTooltipChildren}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
</Widget>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default HealthAndSafety;
|
|
49
|
+
|