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,330 @@
1
+ /* Isolated Mapbox GL CSS - Scoped to prevent Leaflet conflicts */
2
+
3
+ /* Mapbox GL Core Styles - Scoped with .mapbox-gl-scope */
4
+ .mapbox-gl-scope .mapboxgl-map {
5
+ font: 12px/20px Helvetica Neue, Arial, Helvetica, sans-serif;
6
+ overflow: hidden;
7
+ position: relative;
8
+ -webkit-tap-highlight-color: rgb(0 0 0/0);
9
+ }
10
+
11
+ .mapbox-gl-scope .mapboxgl-canvas {
12
+ left: 0;
13
+ position: absolute;
14
+ top: 0;
15
+ }
16
+
17
+ .mapbox-gl-scope .mapboxgl-map:-webkit-full-screen {
18
+ height: 100%;
19
+ width: 100%;
20
+ }
21
+
22
+ .mapbox-gl-scope .mapboxgl-canary {
23
+ background-color: salmon;
24
+ }
25
+
26
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-interactive,
27
+ .mapbox-gl-scope .mapboxgl-ctrl-group button.mapboxgl-ctrl-compass {
28
+ cursor: grab;
29
+ -webkit-user-select: none;
30
+ user-select: none;
31
+ }
32
+
33
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-interactive.mapboxgl-track-pointer {
34
+ cursor: pointer;
35
+ }
36
+
37
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-interactive:active,
38
+ .mapbox-gl-scope .mapboxgl-ctrl-group button.mapboxgl-ctrl-compass:active {
39
+ cursor: grabbing;
40
+ }
41
+
42
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate,
43
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate .mapboxgl-canvas {
44
+ touch-action: pan-x pan-y;
45
+ }
46
+
47
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-drag-pan,
48
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-drag-pan .mapboxgl-canvas {
49
+ touch-action: pinch-zoom;
50
+ }
51
+
52
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan,
53
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan .mapboxgl-canvas {
54
+ touch-action: none;
55
+ }
56
+
57
+ /* Control positioning */
58
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom,
59
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-left,
60
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-right,
61
+ .mapbox-gl-scope .mapboxgl-ctrl-left,
62
+ .mapbox-gl-scope .mapboxgl-ctrl-right,
63
+ .mapbox-gl-scope .mapboxgl-ctrl-top,
64
+ .mapbox-gl-scope .mapboxgl-ctrl-top-left,
65
+ .mapbox-gl-scope .mapboxgl-ctrl-top-right {
66
+ pointer-events: none;
67
+ position: absolute;
68
+ z-index: 2;
69
+ }
70
+
71
+ .mapbox-gl-scope .mapboxgl-ctrl-top-left {
72
+ left: 0;
73
+ top: 0;
74
+ }
75
+
76
+ .mapbox-gl-scope .mapboxgl-ctrl-top {
77
+ left: 50%;
78
+ top: 0;
79
+ transform: translateX(-50%);
80
+ }
81
+
82
+ .mapbox-gl-scope .mapboxgl-ctrl-top-right {
83
+ right: 0;
84
+ top: 0;
85
+ }
86
+
87
+ .mapbox-gl-scope .mapboxgl-ctrl-right {
88
+ right: 0;
89
+ top: 50%;
90
+ transform: translateY(-50%);
91
+ }
92
+
93
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-right {
94
+ bottom: 0;
95
+ right: 0;
96
+ }
97
+
98
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom {
99
+ bottom: 0;
100
+ left: 50%;
101
+ transform: translateX(-50%);
102
+ }
103
+
104
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-left {
105
+ bottom: 0;
106
+ left: 0;
107
+ }
108
+
109
+ .mapbox-gl-scope .mapboxgl-ctrl-left {
110
+ left: 0;
111
+ top: 50%;
112
+ transform: translateY(-50%);
113
+ }
114
+
115
+ .mapbox-gl-scope .mapboxgl-ctrl {
116
+ clear: both;
117
+ pointer-events: auto;
118
+ transform: translate(0);
119
+ }
120
+
121
+ .mapbox-gl-scope .mapboxgl-ctrl-top-left .mapboxgl-ctrl {
122
+ float: left;
123
+ margin: 10px 0 0 10px;
124
+ }
125
+
126
+ .mapbox-gl-scope .mapboxgl-ctrl-top .mapboxgl-ctrl {
127
+ float: left;
128
+ margin: 10px 0;
129
+ }
130
+
131
+ .mapbox-gl-scope .mapboxgl-ctrl-top-right .mapboxgl-ctrl {
132
+ float: right;
133
+ margin: 10px 10px 0 0;
134
+ }
135
+
136
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-right .mapboxgl-ctrl,
137
+ .mapbox-gl-scope .mapboxgl-ctrl-right .mapboxgl-ctrl {
138
+ float: right;
139
+ margin: 0 10px 10px 0;
140
+ }
141
+
142
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom .mapboxgl-ctrl {
143
+ float: left;
144
+ margin: 10px 0;
145
+ }
146
+
147
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-left .mapboxgl-ctrl,
148
+ .mapbox-gl-scope .mapboxgl-ctrl-left .mapboxgl-ctrl {
149
+ float: left;
150
+ margin: 0 0 10px 10px;
151
+ }
152
+
153
+ /* Control group styling */
154
+ .mapbox-gl-scope .mapboxgl-ctrl-group {
155
+ background: #fff;
156
+ border-radius: 4px;
157
+ }
158
+
159
+ .mapbox-gl-scope .mapboxgl-ctrl-group:not(:empty) {
160
+ box-shadow: 0 0 0 2px #0000001a;
161
+ }
162
+
163
+ .mapbox-gl-scope .mapboxgl-ctrl-group button {
164
+ background-color: initial;
165
+ border: 0;
166
+ box-sizing: border-box;
167
+ cursor: pointer;
168
+ display: block;
169
+ height: 29px;
170
+ outline: none;
171
+ overflow: hidden;
172
+ padding: 0;
173
+ width: 29px;
174
+ }
175
+
176
+ .mapbox-gl-scope .mapboxgl-ctrl-group button+button {
177
+ border-top: 1px solid #ddd;
178
+ }
179
+
180
+ .mapbox-gl-scope .mapboxgl-ctrl button .mapboxgl-ctrl-icon {
181
+ background-position: 50%;
182
+ background-repeat: no-repeat;
183
+ display: block;
184
+ height: 100%;
185
+ width: 100%;
186
+ }
187
+
188
+ .mapbox-gl-scope .mapboxgl-ctrl-attrib-button:focus,
189
+ .mapbox-gl-scope .mapboxgl-ctrl-group button:focus {
190
+ box-shadow: 0 0 2px 2px #0096ff;
191
+ }
192
+
193
+ .mapbox-gl-scope .mapboxgl-ctrl button:disabled {
194
+ cursor: not-allowed;
195
+ }
196
+
197
+ .mapbox-gl-scope .mapboxgl-ctrl button:disabled .mapboxgl-ctrl-icon {
198
+ opacity: .25;
199
+ }
200
+
201
+ .mapbox-gl-scope .mapboxgl-ctrl-group button:first-child {
202
+ border-radius: 4px 4px 0 0;
203
+ }
204
+
205
+ .mapbox-gl-scope .mapboxgl-ctrl-group button:last-child {
206
+ border-radius: 0 0 4px 4px;
207
+ }
208
+
209
+ .mapbox-gl-scope .mapboxgl-ctrl-group button:only-child {
210
+ border-radius: inherit;
211
+ }
212
+
213
+ .mapbox-gl-scope .mapboxgl-ctrl button:not(:disabled):hover {
214
+ background-color: #0000000d;
215
+ }
216
+
217
+ /* Marker styles */
218
+ .mapbox-gl-scope .mapboxgl-marker {
219
+ position: absolute;
220
+ z-index: 1;
221
+ }
222
+
223
+ .mapbox-gl-scope .mapboxgl-marker svg {
224
+ display: block;
225
+ }
226
+
227
+ /* Popup styles */
228
+ .mapbox-gl-scope .mapboxgl-popup {
229
+ position: absolute;
230
+ text-align: center;
231
+ margin-bottom: 20px;
232
+ }
233
+
234
+ .mapbox-gl-scope .mapboxgl-popup-content-wrapper {
235
+ padding: 1px;
236
+ text-align: left;
237
+ border-radius: 12px;
238
+ }
239
+
240
+ .mapbox-gl-scope .mapboxgl-popup-content {
241
+ margin: 13px 24px 13px 20px;
242
+ line-height: 1.3;
243
+ font-size: 13px;
244
+ min-height: 1px;
245
+ }
246
+
247
+ .mapbox-gl-scope .mapboxgl-popup-content p {
248
+ margin: 17px 0;
249
+ }
250
+
251
+ .mapbox-gl-scope .mapboxgl-popup-tip-container {
252
+ width: 40px;
253
+ height: 20px;
254
+ position: absolute;
255
+ left: 50%;
256
+ margin-top: -1px;
257
+ margin-left: -20px;
258
+ overflow: hidden;
259
+ pointer-events: none;
260
+ }
261
+
262
+ .mapbox-gl-scope .mapboxgl-popup-tip {
263
+ width: 17px;
264
+ height: 17px;
265
+ padding: 1px;
266
+ margin: -10px auto 0;
267
+ pointer-events: auto;
268
+ -webkit-transform: rotate(45deg);
269
+ -moz-transform: rotate(45deg);
270
+ -ms-transform: rotate(45deg);
271
+ transform: rotate(45deg);
272
+ }
273
+
274
+ .mapbox-gl-scope .mapboxgl-popup-content-wrapper,
275
+ .mapbox-gl-scope .mapboxgl-popup-tip {
276
+ background: white;
277
+ color: #333;
278
+ box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
279
+ }
280
+
281
+ .mapbox-gl-scope .mapboxgl-popup-close-button {
282
+ position: absolute;
283
+ top: 0;
284
+ right: 0;
285
+ border: none;
286
+ text-align: center;
287
+ width: 24px;
288
+ height: 24px;
289
+ font: 16px/24px Tahoma, Verdana, sans-serif;
290
+ color: #757575;
291
+ text-decoration: none;
292
+ background: transparent;
293
+ }
294
+
295
+ .mapbox-gl-scope .mapboxgl-popup-close-button:hover,
296
+ .mapbox-gl-scope .mapboxgl-popup-close-button:focus {
297
+ color: #585858;
298
+ }
299
+
300
+ /* Attribution */
301
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution {
302
+ background: #fff;
303
+ background: rgba(255, 255, 255, 0.8);
304
+ margin: 0;
305
+ }
306
+
307
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution,
308
+ .mapbox-gl-scope .mapboxgl-ctrl-scale-line {
309
+ padding: 0 5px;
310
+ color: #333;
311
+ line-height: 1.4;
312
+ }
313
+
314
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution a {
315
+ text-decoration: none;
316
+ }
317
+
318
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution a:hover,
319
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution a:focus {
320
+ text-decoration: underline;
321
+ }
322
+
323
+ /* Hide attribution by default */
324
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution {
325
+ display: none !important;
326
+ }
327
+
328
+ .mapbox-gl-scope .mapboxgl-ctrl-logo {
329
+ display: none !important;
330
+ }
@@ -13134,6 +13134,63 @@ const renderPercentage = val => {
13134
13134
  return val + "%";
13135
13135
  };
13136
13136
 
13137
+ /**
13138
+ * Calculates stat change object for StatCard component based on current and previous values
13139
+ * @param {Object} data - Object with current and previous values
13140
+ * @param {number} data.current - Current value
13141
+ * @param {number} data.previous - Previous value
13142
+ * @param {Object} options - Optional configuration
13143
+ * @param {string} options.tooltipText - Custom tooltip text
13144
+ * @param {string} options.format - Format type: 'percentage' (default) or 'absolute'
13145
+ * @param {number} options.decimalPlaces - Number of decimal places for percentage (default: 1)
13146
+ * @returns {Object|null} Change object for StatCard or null if data is invalid
13147
+ */
13148
+ const calculateStatChange = (data, options = {}) => {
13149
+ if (!data || typeof data !== 'object') {
13150
+ return null;
13151
+ }
13152
+ const {
13153
+ current,
13154
+ previous
13155
+ } = data;
13156
+
13157
+ // Validate that both values are numbers
13158
+ if (typeof current !== 'number' || typeof previous !== 'number') {
13159
+ return null;
13160
+ }
13161
+
13162
+ // If previous is 0, we can't calculate percentage change
13163
+ if (previous === 0) {
13164
+ return null;
13165
+ }
13166
+ const {
13167
+ tooltipText,
13168
+ format = 'percentage',
13169
+ decimalPlaces = 1
13170
+ } = options;
13171
+
13172
+ // Calculate the difference
13173
+ const difference = current - previous;
13174
+ const isPositive = difference >= 0;
13175
+ const direction = isPositive ? 'up' : 'down';
13176
+
13177
+ // Format the value
13178
+ let value;
13179
+ if (format === 'absolute') {
13180
+ // Show absolute difference
13181
+ value = Math.abs(difference).toLocaleString();
13182
+ } else {
13183
+ // Show percentage change
13184
+ const percentageChange = Math.abs(difference) / previous * 100;
13185
+ value = `${percentageChange.toFixed(decimalPlaces)}%`;
13186
+ }
13187
+ return {
13188
+ value,
13189
+ direction,
13190
+ tooltipText: tooltipText || undefined
13191
+ };
13192
+ };
13193
+
13137
13194
  /**
13138
13195
  * Enum for message types used with Ant Design message component
13139
13196
  * @enum {string}
@@ -14908,6 +14965,7 @@ exports.buildBreadCrumbsHelper = buildBreadCrumbs;
14908
14965
  exports.buildBreadcrumbs = buildBreadcrumbs;
14909
14966
  exports.buildKeyIndicatorsConfig = buildKeyIndicatorsConfig;
14910
14967
  exports.buildQueryString = buildQueryString;
14968
+ exports.calculateStatChange = calculateStatChange;
14911
14969
  exports.camelCaseToTitle = camelCaseToTitle;
14912
14970
  exports.capitalize = capitalize;
14913
14971
  exports.capitalizeAll = capitalizeAll;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datastake-daf",
3
- "version": "0.6.784",
3
+ "version": "0.6.785",
4
4
  "dependencies": {
5
5
  "@ant-design/icons": "^5.2.5",
6
6
  "@antv/g2": "^5.1.1",
@@ -0,0 +1,48 @@
1
+ import { useMemo } from 'react';
2
+ import { KeyIndicators } from '../../../../../../../../src/index.js';
3
+ import { getKeyIndicatorsRowConfig } from '../../config';
4
+ import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
5
+
6
+ const KeyInformation = ({ id, t = () => { }, getSummaryDetail, loading = false }) => {
7
+
8
+ const defaultConfig = useMemo(
9
+ () => ({
10
+ basepath: "events/monitoring-campaign",
11
+ url: `/summary/${id}/key-information`,
12
+ stop: !id,
13
+ }),
14
+ [id],
15
+ );
16
+
17
+ const customGetData = useMemo(() => {
18
+ if (getSummaryDetail && id) {
19
+ return ({ url, params = {} }) => {
20
+ const match = url.match(/\/summary\/[^/]+\/(.+)/);
21
+ if (match) {
22
+ const [, type] = match;
23
+ return getSummaryDetail(id, type, params);
24
+ }
25
+ throw new Error(`Invalid URL format: ${url}`);
26
+ };
27
+ }
28
+ return undefined;
29
+ }, [getSummaryDetail, id]);
30
+
31
+ const { loading: keyInformationLoading, data: keyInformationData } = useWidgetFetch({
32
+ config: defaultConfig,
33
+ getData: customGetData
34
+ });
35
+ const keyIndicatorsConfig = useMemo(() => getKeyIndicatorsRowConfig({ t, data: keyInformationData }), [t, keyInformationData]);
36
+ return (
37
+ <section>
38
+ <KeyIndicators
39
+ title={t("Key Information")}
40
+ config={keyIndicatorsConfig}
41
+ loading={loading || keyInformationLoading}
42
+ />
43
+ </section>
44
+ );
45
+ };
46
+
47
+ export default KeyInformation;
48
+
@@ -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 PlantedSpecies = ({
8
+ plantedSpeciesChart,
9
+ t = (s) => s
10
+ }) => {
11
+ const pieData = useMemo(() => {
12
+ const data = plantedSpeciesChart || [];
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
+ }, [plantedSpeciesChart]);
23
+
24
+ const isEmpty = useMemo(() => {
25
+ return !plantedSpeciesChart || plantedSpeciesChart.length === 0 ||
26
+ plantedSpeciesChart.every(item => !item?.value || Number(item.value) === 0);
27
+ }, [plantedSpeciesChart]);
28
+
29
+ const getTooltipChildren = useCallback(
30
+ (item) => {
31
+ if (isEmpty) {
32
+ return null;
33
+ }
34
+
35
+ return renderTooltipJsx({
36
+ title: t("Planted Species"),
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("Planted Species")}
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 PlantedSpecies;
73
+
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { Widget, ColumnChart } from '../../../../../../../../src/index.js';
3
+
4
+ const SeedlingsHeight = ({
5
+ seedlingsHeightChart,
6
+ t = (s) => s
7
+ }) => {
8
+ return (
9
+ <Widget
10
+ title={t("Seedlings Height")}
11
+ className="with-border-header h-w-btn-header"
12
+ >
13
+ <div className="flex flex-1 flex-column justify-content-center">
14
+ <div className="flex justify-content-center w-full">
15
+ <ColumnChart
16
+ data={seedlingsHeightChart || []}
17
+ xFieldKey="label"
18
+ yFieldKey="value"
19
+ animated={true}
20
+ height={200}
21
+ color="#016C6E"
22
+ renderTooltipContent={(title, data) => {
23
+ if (!data || data.length === 0) return {};
24
+ const item = data[0]?.data || data[0];
25
+ return {
26
+ title: t("Seedlings Height"),
27
+ subTitle: title,
28
+ items: [
29
+ {
30
+ label: t("Count"),
31
+ value: item?.value || 0,
32
+ },
33
+ ],
34
+ };
35
+ }}
36
+ />
37
+ </div>
38
+ </div>
39
+ </Widget>
40
+ );
41
+ };
42
+
43
+ export default SeedlingsHeight;
44
+
@@ -0,0 +1,86 @@
1
+ import React, { useMemo } from 'react';
2
+ import { StatCard } from '../../../../../../../../src/index.js';
3
+ import { calculateStatChange } from '../../../../../../utils/numbers.js';
4
+
5
+ const Stats = ({
6
+ survivalRate,
7
+ averageHeight,
8
+ averageDiameter,
9
+ t = (s) => s
10
+ }) => {
11
+ const survivalRateChange = useMemo(() => {
12
+ if (!survivalRate) return null;
13
+ return calculateStatChange(
14
+ {
15
+ current: Number(survivalRate.current) || 0,
16
+ previous: Number(survivalRate.previous) || 0,
17
+ },
18
+ {
19
+ tooltipText: t("In comparison to last period"),
20
+ format: 'absolute',
21
+ }
22
+ );
23
+ }, [survivalRate, t]);
24
+
25
+ const averageHeightChange = useMemo(() => {
26
+ if (!averageHeight) return null;
27
+ return calculateStatChange(
28
+ {
29
+ current: Number(averageHeight.current) || 0,
30
+ previous: Number(averageHeight.previous) || 0,
31
+ },
32
+ {
33
+ tooltipText: t("In comparison to last period"),
34
+ format: 'absolute',
35
+ }
36
+ );
37
+ }, [averageHeight, t]);
38
+
39
+ const averageDiameterChange = useMemo(() => {
40
+ if (!averageDiameter) return null;
41
+ return calculateStatChange(
42
+ {
43
+ current: Number(averageDiameter.current) || 0,
44
+ previous: Number(averageDiameter.previous) || 0,
45
+ },
46
+ {
47
+ tooltipText: t("In comparison to last period"),
48
+ format: 'absolute',
49
+ }
50
+ );
51
+ }, [averageDiameter, t]);
52
+
53
+ return (
54
+ <div style={{ display: "flex", gap: "24px", marginBottom: "24px" }}>
55
+ <section style={{ flex: 1 }}>
56
+ <StatCard
57
+ title={t("Survival Rate")}
58
+ value={survivalRate ? Number(survivalRate.current).toLocaleString() : "0"}
59
+ icon="EventCalendar"
60
+ change={survivalRateChange}
61
+ />
62
+ </section>
63
+
64
+ <section style={{ flex: 1 }}>
65
+ <StatCard
66
+ title={t("Average Height")}
67
+ value={averageHeight ? Number(averageHeight.current).toLocaleString() + " cm" : "0 cm"}
68
+ icon="ProjectLocation"
69
+ change={averageHeightChange}
70
+ />
71
+ </section>
72
+
73
+ <section style={{ flex: 1 }}>
74
+ <StatCard
75
+ title={t("Average Diameter")}
76
+ value={averageDiameter ? Number(averageDiameter.current).toLocaleString() + " mm" : "0 mm"}
77
+ icon="Activity"
78
+ change={averageDiameterChange}
79
+ />
80
+ </section>
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default Stats;
86
+