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.
Files changed (79) hide show
  1. package/dist/components/index.js +1007 -730
  2. package/dist/layouts/index.js +495 -459
  3. package/dist/pages/index.js +7914 -6836
  4. package/dist/style/datastake/mapbox-gl.css +330 -0
  5. package/dist/utils/index.js +481 -457
  6. package/package.json +1 -1
  7. package/src/@daf/core/components/Charts/ColumnChart/index.jsx +10 -0
  8. package/src/@daf/core/components/Charts/LineChart/index.jsx +14 -0
  9. package/src/@daf/core/components/Dashboard/Map/ChainIcon/Markers/StakeholderMarker.js +5 -2
  10. package/src/@daf/core/components/Dashboard/Map/ChainIcon/index.js +67 -27
  11. package/src/@daf/core/components/Dashboard/Map/hook.js +26 -32
  12. package/src/@daf/core/components/Dashboard/Widget/ActivityIndicators/index.jsx +2 -0
  13. package/src/@daf/core/components/Dashboard/Widget/StatCard/StatCard.stories.js +226 -0
  14. package/src/@daf/core/components/Dashboard/Widget/StatCard/index.js +103 -0
  15. package/src/@daf/core/components/Dashboard/Widget/StatCard/style.js +83 -0
  16. package/src/@daf/core/components/Icon/configs/Down.js +8 -0
  17. package/src/@daf/core/components/Icon/configs/Up.js +8 -0
  18. package/src/@daf/core/components/Icon/configs/index.js +4 -0
  19. package/src/@daf/core/components/Icon/configs/partnerIcon.js +1 -1
  20. package/src/@daf/core/components/Screens/BaseScreen/index.jsx +1 -1
  21. package/src/@daf/core/components/Screens/TableScreen/TablePageWithTabs/index.jsx +1 -1
  22. package/src/@daf/core/components/Sidenav/Menu.jsx +4 -4
  23. package/src/@daf/core/components/UI/MissingTagButton/index.jsx +36 -0
  24. package/src/@daf/pages/Dashboards/SupplyChain/components/SupplyChainMap/index.js +0 -2
  25. package/src/@daf/pages/Documents/config.js +0 -10
  26. package/src/@daf/pages/Documents/index.jsx +51 -108
  27. package/src/@daf/pages/Events/Activities/config.js +1 -11
  28. package/src/@daf/pages/Events/Activities/index.jsx +47 -105
  29. package/src/@daf/pages/Events/Incidents/config.js +1 -11
  30. package/src/@daf/pages/Events/Incidents/index.jsx +47 -105
  31. package/src/@daf/pages/Events/config.js +18 -34
  32. package/src/@daf/pages/Events/index.jsx +49 -111
  33. package/src/@daf/pages/Locations/MineSite/config.js +0 -10
  34. package/src/@daf/pages/Locations/MineSite/index.jsx +47 -105
  35. package/src/@daf/pages/Locations/config.js +4 -16
  36. package/src/@daf/pages/Locations/index.jsx +53 -110
  37. package/src/@daf/pages/Stakeholders/Operators/config.js +0 -10
  38. package/src/@daf/pages/Stakeholders/Operators/index.jsx +47 -105
  39. package/src/@daf/pages/Stakeholders/Workers/config.js +0 -10
  40. package/src/@daf/pages/Stakeholders/Workers/index.jsx +47 -105
  41. package/src/@daf/pages/Stakeholders/config.js +3 -15
  42. package/src/@daf/pages/Stakeholders/index.jsx +53 -109
  43. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx +43 -0
  44. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/CommunityStats/helper.js +60 -0
  45. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/CommunityStats/index.jsx +36 -0
  46. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/GenderDistribution/helper.js +117 -0
  47. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/GenderDistribution/index.jsx +49 -0
  48. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/JobsTimeline/index.jsx +212 -0
  49. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/index.jsx +72 -0
  50. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/CyclePartners/helper.js +91 -0
  51. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/CyclePartners/index.jsx +50 -0
  52. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/helper.js +134 -0
  53. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/index.jsx +49 -0
  54. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/index.jsx +112 -0
  55. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/index.jsx +498 -0
  56. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/KeyInformation/index.jsx +49 -0
  57. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/PlantingLocations/index.jsx +120 -0
  58. package/src/@daf/pages/Summary/Activities/PlantingCycle/config.js +5 -10
  59. package/src/@daf/pages/Summary/Activities/PlantingCycle/helper.js +218 -0
  60. package/src/@daf/pages/Summary/Activities/PlantingCycle/index.jsx +22 -32
  61. package/src/@daf/pages/Summary/Activities/Restoration/components/ActivityImagery/index.jsx +29 -0
  62. package/src/@daf/pages/Summary/Activities/Restoration/components/ActivityLocation/index.jsx +94 -0
  63. package/src/@daf/pages/Summary/Activities/Restoration/components/WorkersDistribution/index.jsx +49 -0
  64. package/src/@daf/pages/Summary/Activities/Restoration/index.jsx +16 -138
  65. package/src/@daf/pages/TablePage/config.js +78 -0
  66. package/src/@daf/{core/components/Screens/TableScreen/TableWithTabsAndCreate → pages/TablePage}/create.jsx +6 -5
  67. package/src/@daf/pages/TablePage/hook.js +123 -0
  68. package/src/@daf/pages/TablePage/index.jsx +142 -0
  69. package/src/index.js +2 -0
  70. package/src/@daf/core/components/Screens/TableScreen/TableWithTabsAndCreate/index.jsx +0 -115
  71. package/src/@daf/pages/Documents/create.jsx +0 -105
  72. package/src/@daf/pages/Events/Activities/create.jsx +0 -104
  73. package/src/@daf/pages/Events/Incidents/create.jsx +0 -104
  74. package/src/@daf/pages/Events/create.jsx +0 -104
  75. package/src/@daf/pages/Locations/MineSite/create.jsx +0 -104
  76. package/src/@daf/pages/Locations/create.jsx +0 -104
  77. package/src/@daf/pages/Stakeholders/Operators/create.jsx +0 -104
  78. package/src/@daf/pages/Stakeholders/Workers/create.jsx +0 -104
  79. 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;
@@ -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
+