datastake-daf 0.6.784 → 0.6.785

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 (21) hide show
  1. package/dist/components/index.js +99 -99
  2. package/dist/pages/index.js +846 -65
  3. package/dist/style/datastake/mapbox-gl.css +330 -0
  4. package/dist/utils/index.js +58 -0
  5. package/package.json +1 -1
  6. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/components/KeyInformation/index.jsx +48 -0
  7. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/components/MangroveGrowth/PlantedSpecies.jsx +73 -0
  8. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/components/MangroveGrowth/SeedlingsHeight.jsx +44 -0
  9. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/components/MangroveGrowth/Stats.jsx +86 -0
  10. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/components/MangroveGrowth/VegetationHealth.jsx +73 -0
  11. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/components/MangroveGrowth/index.jsx +92 -0
  12. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/components/MonitoringScopeAndFindings/index.jsx +348 -0
  13. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/config.js +35 -0
  14. package/src/@daf/pages/Summary/Activities/MonitoringCampaign/index.jsx +30 -0
  15. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/CommunityStats/helper.js +1 -1
  16. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/index.jsx +1 -1
  17. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/index.jsx +1 -1
  18. package/src/@daf/pages/Summary/Activities/PlantingCycle/helper.js +0 -56
  19. package/src/@daf/utils/numbers.js +57 -0
  20. package/src/pages.js +1 -0
  21. package/src/utils.js +1 -1
@@ -0,0 +1,73 @@
1
+ import React, { useMemo, useCallback } from 'react';
2
+ import { Widget, PieChart } from '../../../../../../../../src/index.js';
3
+ import { renderTooltipJsx } from '../../../../../../utils/tooltip.js';
4
+
5
+ const COLORS = ['#016C6E', '#F5C2AC', '#F0A888', '#DF571E', '#C04B19', '#9B3D14', '#7A2F0F'];
6
+
7
+ const VegetationHealth = ({
8
+ vegetationHealthChart,
9
+ t = (s) => s
10
+ }) => {
11
+ const pieData = useMemo(() => {
12
+ const data = vegetationHealthChart || [];
13
+ const total = data.reduce((sum, item) => sum + (Number(item?.value) || 0), 0);
14
+
15
+ return data.map((item, index) => ({
16
+ value: Number(item?.value) || 0,
17
+ percent: total > 0 ? (Number(item?.value) || 0) / total : 0,
18
+ color: COLORS[index % COLORS.length],
19
+ label: item?.type || '',
20
+ key: item?.type || `item-${index}`,
21
+ }));
22
+ }, [vegetationHealthChart]);
23
+
24
+ const isEmpty = useMemo(() => {
25
+ return !vegetationHealthChart || vegetationHealthChart.length === 0 ||
26
+ vegetationHealthChart.every(item => !item?.value || Number(item.value) === 0);
27
+ }, [vegetationHealthChart]);
28
+
29
+ const getTooltipChildren = useCallback(
30
+ (item) => {
31
+ if (isEmpty) {
32
+ return null;
33
+ }
34
+
35
+ return renderTooltipJsx({
36
+ title: t("Vegetation Health"),
37
+ items: [
38
+ {
39
+ color: item.color,
40
+ label: item.label || '',
41
+ value: `${item.value || 0}%`,
42
+ },
43
+ ],
44
+ });
45
+ },
46
+ [t, isEmpty]
47
+ );
48
+
49
+ return (
50
+ <Widget
51
+ title={t("Vegetation Health")}
52
+ className="with-border-header h-w-btn-header"
53
+ >
54
+ <div className="flex flex-1 flex-column justify-content-center">
55
+ <div className="flex justify-content-center w-full">
56
+ <PieChart
57
+ data={pieData}
58
+ isPie
59
+ isEmpty={isEmpty}
60
+ getTooltipChildren={getTooltipChildren}
61
+ mouseXOffset={10}
62
+ mouseYOffset={10}
63
+ changeOpacityOnHover={false}
64
+ doConstraints={false}
65
+ />
66
+ </div>
67
+ </div>
68
+ </Widget>
69
+ );
70
+ };
71
+
72
+ export default VegetationHealth;
73
+
@@ -0,0 +1,92 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Widget } from '../../../../../../../../src/index.js';
3
+ import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
4
+ import VegetationHealth from './VegetationHealth.jsx';
5
+ import SeedlingsHeight from './SeedlingsHeight.jsx';
6
+ import PlantedSpecies from './PlantedSpecies.jsx';
7
+ import Stats from './Stats.jsx';
8
+
9
+ const MangroveGrowth = ({
10
+ id,
11
+ getSummaryDetail,
12
+ loading = false,
13
+ t = (s) => s
14
+ }) => {
15
+ const defaultConfig = useMemo(
16
+ () => ({
17
+ basepath: "events/monitoring-campaign",
18
+ url: `/summary/${id}/outcomes`,
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: outcomesLoading, data: outcomesData } = useWidgetFetch({
39
+ config: defaultConfig,
40
+ getData: customGetData
41
+ });
42
+
43
+ const {
44
+ survivalRate,
45
+ averageHeight,
46
+ averageDiameter,
47
+ vegetationHealthChart,
48
+ seedlingsHeightChart,
49
+ plantedSpeciesChart
50
+ } = outcomesData || {};
51
+
52
+ return (
53
+ <section>
54
+ <Widget
55
+ title={t("Mangrove Growth")}
56
+ loading={loading || outcomesLoading}
57
+ className="with-border-header h-w-btn-header"
58
+ >
59
+ <Stats
60
+ survivalRate={survivalRate}
61
+ averageHeight={averageHeight}
62
+ averageDiameter={averageDiameter}
63
+ t={t}
64
+ />
65
+
66
+ <div style={{ display: "flex", gap: "24px" }}>
67
+ <section style={{ flex: 1 }}>
68
+ <VegetationHealth
69
+ vegetationHealthChart={vegetationHealthChart}
70
+ t={t}
71
+ />
72
+ </section>
73
+ <section style={{ flex: 1 }}>
74
+ <SeedlingsHeight
75
+ seedlingsHeightChart={seedlingsHeightChart}
76
+ t={t}
77
+ />
78
+ </section>
79
+ <section style={{ flex: 1 }}>
80
+ <PlantedSpecies
81
+ plantedSpeciesChart={plantedSpeciesChart}
82
+ t={t}
83
+ />
84
+ </section>
85
+ </div>
86
+ </Widget>
87
+ </section>
88
+ );
89
+ };
90
+
91
+ export default MangroveGrowth;
92
+
@@ -0,0 +1,348 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Widget, MineSiteMap } from '../../../../../../../../src/index.js';
3
+ import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
4
+ import { convertDMS } from '../../../../../../../../src/helpers/Map';
5
+ import CustomIcon from '../../../../../../../../src/@daf/core/components/Icon/CustomIcon.jsx';
6
+ import { renderDateFormatted } from '../../../../../../../../src/helpers/Forms';
7
+
8
+ const VISITS_TAB = "visits";
9
+ const GROWTH_AND_SURVIVAL_TAB = "growthAndSurvival";
10
+ const FAUNA_SIGHTINGS_TAB = "faunaSightings";
11
+ const INVASIVE_SPECIES_TAB = "invasiveSpecies";
12
+ const SYMPTOM_HOTSPOTS_TAB = "symptomHotspots";
13
+ const SOIL_TAB = "soil";
14
+
15
+ const MonitoringScopeAndFindings = ({
16
+ id,
17
+ getSummaryDetail,
18
+ loading = false,
19
+ t = (s) => s
20
+ }) => {
21
+ const [activeTab, setActiveTab] = useState(VISITS_TAB);
22
+
23
+ const defaultConfig = useMemo(
24
+ () => ({
25
+ basepath: "events/monitoring-campaign",
26
+ url: `/summary/${id}/monitoring-scope`,
27
+ stop: !id,
28
+ }),
29
+ [id],
30
+ );
31
+
32
+ const customGetData = useMemo(() => {
33
+ if (getSummaryDetail && id) {
34
+ return ({ url, params = {} }) => {
35
+ const match = url.match(/\/summary\/[^/]+\/(.+)/);
36
+ if (match) {
37
+ const [, type] = match;
38
+ return getSummaryDetail(id, type, { ...params, tab: activeTab });
39
+ }
40
+ throw new Error(`Invalid URL format: ${url}`);
41
+ };
42
+ }
43
+ return undefined;
44
+ }, [getSummaryDetail, id, activeTab]);
45
+
46
+ const { loading: monitoringScopeLoading, data: monitoringScopeData } = useWidgetFetch({
47
+ config: defaultConfig,
48
+ getData: customGetData
49
+ });
50
+
51
+ const filtersConfig = useMemo(() => {
52
+ switch (activeTab) {
53
+ case GROWTH_AND_SURVIVAL_TAB:
54
+ return [
55
+ {
56
+ label: t("Mangrove Survival Rate"),
57
+ placeholder: t("Select"),
58
+ key: "mangroveSurvivalRate",
59
+ type: "slider",
60
+ },
61
+ {
62
+ label: t("Planting Density"),
63
+ placeholder: t("Select"),
64
+ key: "plantingDensity",
65
+ type: "select",
66
+ },
67
+ ];
68
+ case FAUNA_SIGHTINGS_TAB:
69
+ return [
70
+ {
71
+ label: t("Fauna Observed"),
72
+ placeholder: t("Select"),
73
+ key: "faunaSightings",
74
+ type: "select",
75
+ options: [
76
+ { label: t("Birds"), value: "birds" },
77
+ { label: t("Crabs"), value: "crabs" },
78
+ { label: t("Fish"), value: "fish" },
79
+ { label: t("Molluscs"), value: "molluscs" },
80
+ { label: t("Oysters"), value: "oysters" },
81
+ ],
82
+ },
83
+ ];
84
+ case INVASIVE_SPECIES_TAB:
85
+ return [
86
+ {
87
+ label: t("Fauna Observed"),
88
+ placeholder: t("Select"),
89
+ key: "invasiveSpecies",
90
+ type: "select",
91
+ options: [
92
+ { label: t("Spiders"), value: "spiders" },
93
+ { label: t("Scale insects"), value: "scaleInsects" },
94
+ { label: t("Caterpillars"), value: "caterpillars" },
95
+ { label: t("Unidentified pests"), value: "unidentifiedPests" },
96
+ { label: t("Other"), value: "other" },
97
+ ],
98
+ },
99
+ ];
100
+ case SYMPTOM_HOTSPOTS_TAB:
101
+ return [
102
+ {
103
+ label: t("Symptom Hotspots"),
104
+ placeholder: t("Select"),
105
+ key: "symptomHotspots",
106
+ type: "select",
107
+ options: [
108
+ { label: t("Reddish spots on leaves"), value: "reddishSpotsOnLeaves" },
109
+ { label: t("Black spots on leaves"), value: "blackSpotsOnLeaves" },
110
+ { label: t("Yellowing of leaves"), value: "yellowingOfLeaves" },
111
+ { label: t("Presence of mosaic"), value: "presenceOfMosaic" },
112
+ ],
113
+ },
114
+ ];
115
+ case SOIL_TAB:
116
+ return [
117
+ {
118
+ label: t("Soil"),
119
+ placeholder: t("Select"),
120
+ key: "soil",
121
+ type: "select",
122
+ options: [
123
+ { label: t("Sandy"), value: "sandy" },
124
+ { label: t("Clay"), value: "clay" },
125
+ { label: t("Muddy"), value: "muddy" },
126
+ { label: t("Loamy"), value: "loamy" },
127
+ { label: t("Mixed"), value: "mixed" },
128
+ ],
129
+ },
130
+ ];
131
+ default:
132
+ return [];
133
+ }
134
+ }, [activeTab, t]);
135
+
136
+ const mappedData = useMemo(() => {
137
+ if (!monitoringScopeData || !monitoringScopeData.plots) {
138
+ return [];
139
+ }
140
+
141
+ const { plots = [], monitoringActivities = [] } = monitoringScopeData;
142
+
143
+ if (activeTab === VISITS_TAB) {
144
+ return plots.map((plot, index) => {
145
+ const area = plot?.perimeter ? plot.perimeter.map(coord =>
146
+ Array.isArray(coord) && coord.length >= 2
147
+ ? [coord[1], coord[0]]
148
+ : coord
149
+ ) : null;
150
+
151
+ const validArea = area && Array.isArray(area) && area.length >= 3 ? area : null;
152
+
153
+ const matchingActivity = monitoringActivities?.find(activity =>
154
+ activity.plotId === plot.id || activity.plotId === plot._id
155
+ );
156
+
157
+ const gps = matchingActivity?.locationCheckArrival ? {
158
+ latitude: matchingActivity.locationCheckArrival.latitude,
159
+ longitude: matchingActivity.locationCheckArrival.longitude
160
+ } : null;
161
+
162
+ return {
163
+ _id: plot._id || {},
164
+ area: validArea,
165
+ color: "#15FFFFB2",
166
+ datastakeId: plot.datastakeId || `PLOT-${String(index + 1).padStart(9, '0')}`,
167
+ gps: gps,
168
+ id: plot.id || plot._id || `plot-${index}`,
169
+ name: plot.name || t("Plot"),
170
+ date: matchingActivity?.date,
171
+ subTitle: matchingActivity?.date
172
+ ? renderDateFormatted(matchingActivity.date, "DD MMM YY")
173
+ : plot.name,
174
+ plotName: plot.name,
175
+ territoryTitle: plot.name,
176
+ type: plot.type || 'Operational Plot',
177
+ lastVisit: matchingActivity?.date,
178
+ implementer: matchingActivity?.implementer,
179
+ areaHa: plot.area,
180
+ };
181
+ });
182
+ }
183
+
184
+ return plots.map((plot, index) => {
185
+ const area = plot?.perimeter ? plot.perimeter.map(coord =>
186
+ Array.isArray(coord) && coord.length >= 2
187
+ ? [coord[1], coord[0]]
188
+ : coord
189
+ ) : null;
190
+
191
+ const validArea = area && Array.isArray(area) && area.length >= 3 ? area : null;
192
+
193
+ return {
194
+ _id: plot._id || {},
195
+ area: validArea,
196
+ color: "#15FFFFB2",
197
+ datastakeId: plot.datastakeId || `PLOT-${String(index + 1).padStart(9, '0')}`,
198
+ id: plot.id || plot._id || `plot-${index}`,
199
+ name: plot.name || t("Plot"),
200
+ plotName: plot.name,
201
+ territoryTitle: plot.name,
202
+ type: plot.type || 'Operational Plot',
203
+ };
204
+ });
205
+ }, [monitoringScopeData, activeTab, t]);
206
+
207
+ return (
208
+ <section>
209
+ <Widget
210
+ title={t("Monitoring Scope & Findings")}
211
+ className="v2-widget no-px no-p-body h-w-btn-header with-border-header"
212
+ style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
213
+ tabsConfig={{
214
+ tabs: [
215
+ {
216
+ label: t("Visits"),
217
+ value: VISITS_TAB,
218
+ },
219
+ {
220
+ label: t("Growth & Survival"),
221
+ value: GROWTH_AND_SURVIVAL_TAB,
222
+ },
223
+ {
224
+ label: t("Fauna Sightings"),
225
+ value: FAUNA_SIGHTINGS_TAB,
226
+ },
227
+ {
228
+ label: t("Invasive Species"),
229
+ value: INVASIVE_SPECIES_TAB,
230
+ },
231
+ {
232
+ label: t("Symptom Hotspots"),
233
+ value: SYMPTOM_HOTSPOTS_TAB,
234
+ },
235
+ {
236
+ label: t("Soil"),
237
+ value: SOIL_TAB,
238
+ },
239
+ ],
240
+ value: activeTab,
241
+ onChange: setActiveTab,
242
+ }}
243
+ >
244
+ <MineSiteMap
245
+ data={mappedData}
246
+ link={false}
247
+ style={{ height: '100%', width: '100%' }}
248
+ maxZoom={18}
249
+ isSatellite={true}
250
+ onClickLink={() => { }}
251
+ onFilterChange={() => { }}
252
+ primaryLink
253
+ showSider={false}
254
+ filtersConfig={filtersConfig}
255
+ renderTooltipForLocation={(data) => {
256
+ if (activeTab === VISITS_TAB && data.gps) {
257
+ const coordinates = data.gps?.latitude && data.gps?.longitude
258
+ ? convertDMS(data.gps.latitude, data.gps.longitude)
259
+ : null;
260
+
261
+ if (!coordinates) {
262
+ return [];
263
+ }
264
+
265
+ const iconColor = "#016C6E";
266
+
267
+ const tooltipItems = [
268
+ {
269
+ label: t("Coordinates"),
270
+ value: (
271
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'nowrap' }}>
272
+ <div style={{ display: 'flex', alignItems: 'center' }}>
273
+ <CustomIcon
274
+ name="SpacingHeight"
275
+ width={14}
276
+ height={14}
277
+ color={iconColor}
278
+ />
279
+ <span style={{ fontWeight: 600, marginLeft: '4px' }}>{coordinates[0]}</span>
280
+ </div>
281
+ <div style={{ display: 'flex', alignItems: 'center' }}>
282
+ <CustomIcon
283
+ name="SpacingWidth"
284
+ width={14}
285
+ height={14}
286
+ color={iconColor}
287
+ />
288
+ <span style={{ fontWeight: 600, marginLeft: '4px' }}>{coordinates[1]}</span>
289
+ </div>
290
+ </div>
291
+ ),
292
+ },
293
+ ];
294
+
295
+ if (data.date) {
296
+ tooltipItems.push({
297
+ label: t("Date"),
298
+ value: renderDateFormatted(data.date, "DD MMM YY"),
299
+ });
300
+ }
301
+
302
+ if (data.implementer) {
303
+ tooltipItems.push({
304
+ label: t("Implementer"),
305
+ value: data.implementer,
306
+ });
307
+ }
308
+
309
+ return tooltipItems;
310
+ }
311
+
312
+ return [];
313
+ }}
314
+ renderTooltipForTerritory={(data) => {
315
+ const items = [
316
+ {
317
+ label: t("Plot Name"),
318
+ value: data.plotName || data.name || "--",
319
+ },
320
+ ];
321
+
322
+ if (activeTab === VISITS_TAB && data.lastVisit) {
323
+ items.push({
324
+ label: t("Last visit"),
325
+ value: renderDateFormatted(data.lastVisit, "DD MMM YY"),
326
+ });
327
+ }
328
+
329
+ if (activeTab === VISITS_TAB && data.areaHa) {
330
+ items.push({
331
+ label: t("Area"),
332
+ value: `${data.areaHa} ha`,
333
+ });
334
+ }
335
+
336
+ return items;
337
+ }}
338
+ renderTooltipTags={() => { }}
339
+ type="location-territory"
340
+ loading={loading || monitoringScopeLoading}
341
+ />
342
+ </Widget>
343
+ </section>
344
+ );
345
+ };
346
+
347
+ export default MonitoringScopeAndFindings;
348
+
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+
3
+ export const getKeyIndicatorsRowConfig = ({ t, data = {} }) => [
4
+ {
5
+ label: t('Region'),
6
+ render: () => {
7
+ return <div>{ data?.region || '-'}</div>;
8
+ }
9
+ },
10
+ {
11
+ label: t('Associated Plots'),
12
+ render: () => {
13
+ return <div>{data?.associatedPlotsCount || '0'}</div>
14
+ }
15
+ },
16
+ {
17
+ label: t('Implementation Partners'),
18
+ render: () => {
19
+ return <div>{data?.partnersCount || '0'}</div>
20
+ }
21
+ },
22
+ {
23
+ label: t('Total Activities'),
24
+ render: () => {
25
+ return <div>{data?.activitiesCount || '0'}</div>
26
+ }
27
+ },
28
+ {
29
+ label: t('Information Sources'),
30
+ render: () => {
31
+ return <div>{data?.informationSourcesCount || '0'}</div>
32
+ }
33
+ },
34
+ ];
35
+
@@ -0,0 +1,30 @@
1
+ import { DashboardLayout, Header } from '../../../../../../src/index.js'
2
+ import KeyInformation from './components/KeyInformation/index.jsx';
3
+ import MonitoringScopeAndFindings from './components/MonitoringScopeAndFindings/index.jsx';
4
+ import MangroveGrowth from './components/MangroveGrowth/index.jsx';
5
+
6
+ const MonitoringCampaignSummary = ({ header, activityData, loading = false, id, projectId, t = () => { }, getSummaryDetail, navigate, selectOptions }) => {
7
+ return (
8
+ <DashboardLayout
9
+ header={
10
+ <Header
11
+ title={header?.title + ' Summary' || ''}
12
+ supportText={header?.supportText || ''}
13
+ onDownload={header?.onDownload}
14
+ downloadDisabled={header?.downloadDisabled}
15
+ actionButtons={header?.actionButtons}
16
+ breadcrumbs={header?.breadcrumbs}
17
+ goBackTo={header?.goBackTo}
18
+ loading={header?.loading}
19
+ />
20
+ }
21
+ >
22
+ <KeyInformation id={id} t={t} getSummaryDetail={getSummaryDetail} loading={loading} />
23
+ <MonitoringScopeAndFindings id={id} t={t} getSummaryDetail={getSummaryDetail} loading={loading} />
24
+ <MangroveGrowth id={id} t={t} getSummaryDetail={getSummaryDetail} loading={loading} />
25
+ </DashboardLayout>
26
+ )
27
+ }
28
+
29
+ export default MonitoringCampaignSummary;
30
+
@@ -1,4 +1,4 @@
1
- import { calculateStatChange } from '../../../helper.js';
1
+ import { calculateStatChange } from '../../../../../../../utils/numbers.js';
2
2
 
3
3
  /**
4
4
  * Calculates the change for day jobs stat
@@ -3,7 +3,7 @@ import { Widget, StatCard } from '../../../../../../../index.js';
3
3
  import CyclePartners from './CyclePartners/index.jsx';
4
4
  import HealthAndSafety from './HealthAndSafety/index.jsx';
5
5
  import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
6
- import { calculateStatChange } from '../../helper.js';
6
+ import { calculateStatChange } from '../../../../../../utils/numbers.js';
7
7
 
8
8
  const CycleIndicators = ({
9
9
  id,
@@ -1,7 +1,7 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { Widget, StatCard } from '../../../../../../../../src/index.js';
3
3
  import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
4
- import { calculateStatChange } from '../../helper.js';
4
+ import { calculateStatChange } from '../../../../../../utils/numbers.js';
5
5
  import RestoredArea from './RestoredArea.jsx';
6
6
  import PlantingActivitiesTimeline from './PlantingActivitiesTimeline.jsx';
7
7
 
@@ -281,60 +281,4 @@ export const getHealthAndSafetyTooltipChildren = (item, isEmpty, healthAndSafety
281
281
  });
282
282
  };
283
283
 
284
- /**
285
- * Calculates stat change object for StatCard component based on current and previous values
286
- * @param {Object} data - Object with current and previous values
287
- * @param {number} data.current - Current value
288
- * @param {number} data.previous - Previous value
289
- * @param {Object} options - Optional configuration
290
- * @param {string} options.tooltipText - Custom tooltip text
291
- * @param {string} options.format - Format type: 'percentage' (default) or 'absolute'
292
- * @param {number} options.decimalPlaces - Number of decimal places for percentage (default: 1)
293
- * @returns {Object|null} Change object for StatCard or null if data is invalid
294
- */
295
- export const calculateStatChange = (data, options = {}) => {
296
- if (!data || typeof data !== 'object') {
297
- return null;
298
- }
299
-
300
- const { current, previous } = data;
301
-
302
- // Validate that both values are numbers
303
- if (typeof current !== 'number' || typeof previous !== 'number') {
304
- return null;
305
- }
306
-
307
- // If previous is 0, we can't calculate percentage change
308
- if (previous === 0) {
309
- return null;
310
- }
311
-
312
- const {
313
- tooltipText,
314
- format = 'percentage',
315
- decimalPlaces = 1,
316
- } = options;
317
-
318
- // Calculate the difference
319
- const difference = current - previous;
320
- const isPositive = difference >= 0;
321
- const direction = isPositive ? 'up' : 'down';
322
-
323
- // Format the value
324
- let value;
325
- if (format === 'absolute') {
326
- // Show absolute difference
327
- value = Math.abs(difference).toLocaleString();
328
- } else {
329
- // Show percentage change
330
- const percentageChange = (Math.abs(difference) / previous) * 100;
331
- value = `${percentageChange.toFixed(decimalPlaces)}%`;
332
- }
333
-
334
- return {
335
- value,
336
- direction,
337
- tooltipText: tooltipText || undefined,
338
- };
339
- };
340
284