datastake-daf 0.6.768 → 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 +767 -551
- package/dist/layouts/index.js +495 -459
- package/dist/pages/index.js +2808 -738
- 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/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/Sidenav/Menu.jsx +4 -4
- 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/index.js +1 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { Widget, StatCard, LineChart, ColumnChart } from '../../../../../../../../src/index.js';
|
|
4
|
+
import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
|
|
5
|
+
import { calculateStatChange } from '../../helper.js';
|
|
6
|
+
|
|
7
|
+
const CycleOutcomes = ({
|
|
8
|
+
id,
|
|
9
|
+
getSummaryDetail,
|
|
10
|
+
loading = false,
|
|
11
|
+
t = (s) => s
|
|
12
|
+
}) => {
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const defaultConfig = useMemo(
|
|
16
|
+
() => ({
|
|
17
|
+
basepath: "planting-cycle",
|
|
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: dataDetailsLoading, data: dataDetails } = useWidgetFetch({
|
|
39
|
+
config: defaultConfig,
|
|
40
|
+
getData: customGetData
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const { activitiesTimelineChart, locationsCount, restoredAreaChart, totalAreaRestored
|
|
44
|
+
} = dataDetails;
|
|
45
|
+
|
|
46
|
+
const totalAreaRestoredChange = useMemo(() => {
|
|
47
|
+
if (!totalAreaRestored) return null;
|
|
48
|
+
return calculateStatChange(
|
|
49
|
+
{
|
|
50
|
+
current: Number(totalAreaRestored.current) || 0,
|
|
51
|
+
previous: Number(totalAreaRestored.previous) || 0,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
tooltipText: t("In comparison to last cycle"),
|
|
55
|
+
format: 'absolute',
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}, [totalAreaRestored, t]);
|
|
59
|
+
|
|
60
|
+
const locationsCountChange = useMemo(() => {
|
|
61
|
+
if (!locationsCount) return null;
|
|
62
|
+
return calculateStatChange(
|
|
63
|
+
{
|
|
64
|
+
current: Number(locationsCount.current) || 0,
|
|
65
|
+
previous: Number(locationsCount.previous) || 0,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
tooltipText: t("In comparison to last cycle"),
|
|
69
|
+
format: 'absolute',
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}, [locationsCount, t]);
|
|
73
|
+
|
|
74
|
+
// Map restoredAreaChart data to LineChart format
|
|
75
|
+
// Y-axis: total/cumulated value
|
|
76
|
+
// X-axis: date
|
|
77
|
+
// Fill all months in the range, even if empty
|
|
78
|
+
const restoredAreaChartData = useMemo(() => {
|
|
79
|
+
// Always show last 12 months, even if no data
|
|
80
|
+
const now = dayjs().startOf('month');
|
|
81
|
+
const twelveMonthsAgo = now.subtract(11, 'month'); // 11 months ago + current month = 12 months
|
|
82
|
+
|
|
83
|
+
// Create a map of existing data by month (YYYY-MM format)
|
|
84
|
+
const dataMap = new Map();
|
|
85
|
+
const dates = [];
|
|
86
|
+
|
|
87
|
+
// Process restored area data if available
|
|
88
|
+
if (restoredAreaChart && Array.isArray(restoredAreaChart) && restoredAreaChart.length > 0) {
|
|
89
|
+
restoredAreaChart.forEach((item) => {
|
|
90
|
+
if (typeof item === 'object' && item !== null) {
|
|
91
|
+
// Priority: look for date field first
|
|
92
|
+
const dateValue = item.date || item.label || item.name || item.period || item.month;
|
|
93
|
+
if (dateValue) {
|
|
94
|
+
const date = dayjs(dateValue);
|
|
95
|
+
if (date.isValid()) {
|
|
96
|
+
const monthKey = date.format('YYYY-MM');
|
|
97
|
+
// Total/cumulated value for Y-axis (this is what gets plotted)
|
|
98
|
+
const totalValue = Number(item.cumulated || item.cumulative || item.total || item.value || 0) || 0;
|
|
99
|
+
// Period value for tooltip only
|
|
100
|
+
const periodValue = Number(item.period || item.area || item.count || 0) || 0;
|
|
101
|
+
dates.push(date);
|
|
102
|
+
|
|
103
|
+
// If multiple entries for same month, use the latest one (or sum if needed)
|
|
104
|
+
if (!dataMap.has(monthKey) || dataMap.get(monthKey).value < totalValue) {
|
|
105
|
+
dataMap.set(monthKey, {
|
|
106
|
+
date: dateValue,
|
|
107
|
+
value: totalValue,
|
|
108
|
+
period: periodValue,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Determine date range
|
|
118
|
+
let minDate = twelveMonthsAgo;
|
|
119
|
+
let maxDate = now;
|
|
120
|
+
|
|
121
|
+
// If we have data, adjust range to include it
|
|
122
|
+
if (dates.length > 0) {
|
|
123
|
+
const sortedDates = dates.sort((a, b) => a.valueOf() - b.valueOf());
|
|
124
|
+
const firstDataDate = sortedDates[0].startOf('month');
|
|
125
|
+
const lastDataDate = sortedDates[sortedDates.length - 1].startOf('month');
|
|
126
|
+
|
|
127
|
+
// Start from the earlier of: 12 months ago, or first data date
|
|
128
|
+
minDate = twelveMonthsAgo.isBefore(firstDataDate) ? twelveMonthsAgo : firstDataDate;
|
|
129
|
+
|
|
130
|
+
// End at the later of: current month, or last data date
|
|
131
|
+
maxDate = now.isAfter(lastDataDate) ? now : lastDataDate;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Generate all months in the range
|
|
135
|
+
const result = [];
|
|
136
|
+
let currentDate = minDate.clone();
|
|
137
|
+
let lastKnownValue = 0; // For cumulative data, carry forward the last known value
|
|
138
|
+
|
|
139
|
+
while (currentDate.isBefore(maxDate) || currentDate.isSame(maxDate, 'month')) {
|
|
140
|
+
const monthKey = currentDate.format('YYYY-MM');
|
|
141
|
+
const existingData = dataMap.get(monthKey);
|
|
142
|
+
|
|
143
|
+
if (existingData) {
|
|
144
|
+
lastKnownValue = existingData.value;
|
|
145
|
+
result.push(existingData);
|
|
146
|
+
} else {
|
|
147
|
+
// Fill missing month - for cumulative data, use last known value
|
|
148
|
+
result.push({
|
|
149
|
+
date: currentDate.format('YYYY-MM-DD'),
|
|
150
|
+
value: lastKnownValue, // Carry forward cumulative value
|
|
151
|
+
period: 0,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
currentDate = currentDate.add(1, 'month');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}, [restoredAreaChart]);
|
|
160
|
+
|
|
161
|
+
// Calculate max value for yAxis dynamically (round up to nearest 10)
|
|
162
|
+
const maxYValue = useMemo(() => {
|
|
163
|
+
if (!restoredAreaChartData || restoredAreaChartData.length === 0) {
|
|
164
|
+
return 80;
|
|
165
|
+
}
|
|
166
|
+
const maxValue = Math.max(...restoredAreaChartData.map(item => item.value || 0));
|
|
167
|
+
// If max is 0, set default to 80 to show Y-axis
|
|
168
|
+
// Otherwise, round up to nearest 10
|
|
169
|
+
if (maxValue === 0) {
|
|
170
|
+
return 80;
|
|
171
|
+
}
|
|
172
|
+
return Math.ceil(maxValue / 10) * 10 || 80;
|
|
173
|
+
}, [restoredAreaChartData]);
|
|
174
|
+
|
|
175
|
+
// Format date to "Mmm YY" format for X-axis
|
|
176
|
+
const formatDateAxis = useMemo(() => {
|
|
177
|
+
return (label) => {
|
|
178
|
+
if (!label) return label;
|
|
179
|
+
|
|
180
|
+
// Try to parse the date using dayjs with various formats
|
|
181
|
+
let date = dayjs(label);
|
|
182
|
+
|
|
183
|
+
// If first attempt fails, try parsing as ISO date string
|
|
184
|
+
if (!date.isValid() && typeof label === 'string') {
|
|
185
|
+
date = dayjs(label, ['YYYY-MM-DD', 'YYYY-MM', 'MMM YY', 'MMM YYYY'], true);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If it's a valid date, format it as "Mmm YY"
|
|
189
|
+
if (date.isValid()) {
|
|
190
|
+
return date.format('MMM YY');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If it's already in "Mmm YY" format or similar, return as is
|
|
194
|
+
// Otherwise return the original label
|
|
195
|
+
return label;
|
|
196
|
+
};
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
// Map activitiesTimelineChart data to ColumnChart format
|
|
200
|
+
// Data structure: [{count: 2, date: "2025-11-03"}]
|
|
201
|
+
// Fill all months in the range, even if empty
|
|
202
|
+
const activitiesTimelineData = useMemo(() => {
|
|
203
|
+
// Always show last 12 months, even if no data
|
|
204
|
+
const now = dayjs().startOf('month');
|
|
205
|
+
const twelveMonthsAgo = now.subtract(11, 'month'); // 11 months ago + current month = 12 months
|
|
206
|
+
|
|
207
|
+
// Create a map of existing data by month (YYYY-MM format)
|
|
208
|
+
const dataMap = new Map();
|
|
209
|
+
const dates = [];
|
|
210
|
+
|
|
211
|
+
// Process activities timeline data if available
|
|
212
|
+
if (activitiesTimelineChart && Array.isArray(activitiesTimelineChart) && activitiesTimelineChart.length > 0) {
|
|
213
|
+
activitiesTimelineChart.forEach((item) => {
|
|
214
|
+
if (typeof item === 'object' && item !== null && item.date) {
|
|
215
|
+
const date = dayjs(item.date);
|
|
216
|
+
if (date.isValid()) {
|
|
217
|
+
const monthKey = date.format('YYYY-MM');
|
|
218
|
+
const count = Number(item.count || item.jobs || item.value || 0) || 0;
|
|
219
|
+
dates.push(date);
|
|
220
|
+
|
|
221
|
+
// If multiple entries for same month, sum them
|
|
222
|
+
if (dataMap.has(monthKey)) {
|
|
223
|
+
dataMap.set(monthKey, {
|
|
224
|
+
...dataMap.get(monthKey),
|
|
225
|
+
jobs: dataMap.get(monthKey).jobs + count,
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
dataMap.set(monthKey, {
|
|
229
|
+
month: item.date,
|
|
230
|
+
jobs: count,
|
|
231
|
+
date: item.date,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Determine date range
|
|
240
|
+
let minDate = twelveMonthsAgo;
|
|
241
|
+
let maxDate = now;
|
|
242
|
+
|
|
243
|
+
// If we have data, adjust range to include it
|
|
244
|
+
if (dates.length > 0) {
|
|
245
|
+
const sortedDates = dates.sort((a, b) => a.valueOf() - b.valueOf());
|
|
246
|
+
const firstDataDate = sortedDates[0].startOf('month');
|
|
247
|
+
const lastDataDate = sortedDates[sortedDates.length - 1].startOf('month');
|
|
248
|
+
|
|
249
|
+
// Start from the earlier of: 12 months ago, or first data date
|
|
250
|
+
minDate = twelveMonthsAgo.isBefore(firstDataDate) ? twelveMonthsAgo : firstDataDate;
|
|
251
|
+
|
|
252
|
+
// End at the later of: current month, or last data date
|
|
253
|
+
maxDate = now.isAfter(lastDataDate) ? now : lastDataDate;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Generate all months in the range
|
|
257
|
+
const result = [];
|
|
258
|
+
let currentDate = minDate.clone();
|
|
259
|
+
|
|
260
|
+
while (currentDate.isBefore(maxDate) || currentDate.isSame(maxDate, 'month')) {
|
|
261
|
+
const monthKey = currentDate.format('YYYY-MM');
|
|
262
|
+
const existingData = dataMap.get(monthKey);
|
|
263
|
+
|
|
264
|
+
if (existingData) {
|
|
265
|
+
result.push(existingData);
|
|
266
|
+
} else {
|
|
267
|
+
// Fill missing month with 0
|
|
268
|
+
result.push({
|
|
269
|
+
month: currentDate.format('YYYY-MM-DD'), // Use first day of month
|
|
270
|
+
jobs: 0,
|
|
271
|
+
date: currentDate.format('YYYY-MM-DD'),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
currentDate = currentDate.add(1, 'month');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return result;
|
|
279
|
+
}, [activitiesTimelineChart]);
|
|
280
|
+
|
|
281
|
+
// Calculate max value for Y-axis (default to 100 if all values are 0 or very small)
|
|
282
|
+
const maxActivitiesYValue = useMemo(() => {
|
|
283
|
+
if (!activitiesTimelineData || activitiesTimelineData.length === 0) {
|
|
284
|
+
return 100;
|
|
285
|
+
}
|
|
286
|
+
const maxValue = Math.max(...activitiesTimelineData.map(item => item.jobs || 0));
|
|
287
|
+
// If max is 0, set default to 100 to show Y-axis
|
|
288
|
+
if (maxValue === 0) {
|
|
289
|
+
return 100;
|
|
290
|
+
}
|
|
291
|
+
// Round up to nearest 10, but ensure minimum of 100
|
|
292
|
+
const roundedMax = Math.ceil(maxValue / 10) * 10;
|
|
293
|
+
return Math.max(100, roundedMax);
|
|
294
|
+
}, [activitiesTimelineData]);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<section>
|
|
298
|
+
<Widget
|
|
299
|
+
title={t("Restoration Cycle Outcomes")}
|
|
300
|
+
loading={loading}
|
|
301
|
+
className="with-border-header h-w-btn-header"
|
|
302
|
+
>
|
|
303
|
+
<div style={{ display: "flex", gap: "24px", marginBottom: "24px" }}>
|
|
304
|
+
<section style={{ flex: 1 }}>
|
|
305
|
+
<StatCard
|
|
306
|
+
title={t("Total Area Restored")}
|
|
307
|
+
value={totalAreaRestored?Number(totalAreaRestored.current).toLocaleString() : 0}
|
|
308
|
+
icon="Tree"
|
|
309
|
+
change={totalAreaRestoredChange}
|
|
310
|
+
/>
|
|
311
|
+
</section>
|
|
312
|
+
|
|
313
|
+
<section style={{ flex: 1 }}>
|
|
314
|
+
<StatCard
|
|
315
|
+
title={t("Associated Plots")}
|
|
316
|
+
value={locationsCount?Number(locationsCount.current).toLocaleString() : 0}
|
|
317
|
+
icon="ProjectLocation"
|
|
318
|
+
change={locationsCountChange}
|
|
319
|
+
/>
|
|
320
|
+
</section>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div style={{ display: "flex", gap: "24px" }}>
|
|
324
|
+
<section style={{ flex: 1 }}>
|
|
325
|
+
<Widget
|
|
326
|
+
title={t("Restored Area")}
|
|
327
|
+
className="with-border-header h-w-btn-header"
|
|
328
|
+
>
|
|
329
|
+
<div className="flex flex-1 flex-column justify-content-center">
|
|
330
|
+
<div className="flex justify-content-center w-full">
|
|
331
|
+
<LineChart
|
|
332
|
+
animated
|
|
333
|
+
isArea
|
|
334
|
+
color={'#00AEB1'}
|
|
335
|
+
data={restoredAreaChartData}
|
|
336
|
+
fillOpacity={0.7}
|
|
337
|
+
height={200}
|
|
338
|
+
renderTooltipContent={(title, data) => {
|
|
339
|
+
if (!data || data.length === 0) return {};
|
|
340
|
+
const item = data[0]?.data || data[0];
|
|
341
|
+
const periodValue = item?.period !== undefined ? item.period : (item?.value || 0);
|
|
342
|
+
const cumulatedValue = item?.value || 0;
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
title: t("Restored Area"),
|
|
346
|
+
subTitle: formatDateAxis(title),
|
|
347
|
+
items: [
|
|
348
|
+
{
|
|
349
|
+
label: t("Period"),
|
|
350
|
+
value: `${periodValue.toLocaleString()} ha`,
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
label: t("Cumulated"),
|
|
354
|
+
value: `${cumulatedValue.toLocaleString()} ha`,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}}
|
|
359
|
+
xFieldKey="date"
|
|
360
|
+
yFieldKey="value"
|
|
361
|
+
formattedXAxis={formatDateAxis}
|
|
362
|
+
style={{ width: '100%' }}
|
|
363
|
+
yAxis={{
|
|
364
|
+
min: 0,
|
|
365
|
+
max: maxYValue,
|
|
366
|
+
tickMethod: () => {
|
|
367
|
+
// Generate ticks every 10 units for maxYValue of 80
|
|
368
|
+
// For other values, show ticks every 10 units
|
|
369
|
+
const step = maxYValue <= 80 ? 10 : Math.max(10, Math.floor(maxYValue / 8));
|
|
370
|
+
const ticks = [];
|
|
371
|
+
for (let i = 0; i <= maxYValue; i += step) {
|
|
372
|
+
ticks.push(i);
|
|
373
|
+
}
|
|
374
|
+
// Ensure max value is included
|
|
375
|
+
if (ticks.length === 0 || ticks[ticks.length - 1] < maxYValue) {
|
|
376
|
+
ticks.push(maxYValue);
|
|
377
|
+
}
|
|
378
|
+
return ticks;
|
|
379
|
+
},
|
|
380
|
+
label: {
|
|
381
|
+
style: {
|
|
382
|
+
fontSize: 12,
|
|
383
|
+
fill: '#666',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
grid: {
|
|
387
|
+
line: {
|
|
388
|
+
style: {
|
|
389
|
+
stroke: '#E5E7EB',
|
|
390
|
+
lineWidth: 1,
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
}}
|
|
395
|
+
xAxis={{
|
|
396
|
+
label: {
|
|
397
|
+
formatter: formatDateAxis, // Ensure formatter is applied
|
|
398
|
+
style: {
|
|
399
|
+
fontSize: 12,
|
|
400
|
+
fill: '#666',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
}}
|
|
404
|
+
/>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</Widget>
|
|
408
|
+
</section>
|
|
409
|
+
<section style={{ flex: 1 }}>
|
|
410
|
+
<Widget
|
|
411
|
+
title={t("Planting Activities Timeline")}
|
|
412
|
+
className="with-border-header h-w-btn-header"
|
|
413
|
+
>
|
|
414
|
+
<div className="flex flex-1 flex-column justify-content-center">
|
|
415
|
+
<div className="flex justify-content-center w-full">
|
|
416
|
+
<ColumnChart
|
|
417
|
+
data={activitiesTimelineData}
|
|
418
|
+
xFieldKey="month"
|
|
419
|
+
yFieldKey="jobs"
|
|
420
|
+
animated={true}
|
|
421
|
+
height={400}
|
|
422
|
+
color="#016C6E"
|
|
423
|
+
renderTooltipContent={(title, data) => {
|
|
424
|
+
if (!data || data.length === 0) return {};
|
|
425
|
+
// For ColumnChart, data structure: data[0]?.data contains the actual data point
|
|
426
|
+
const item = data[0]?.data || data[0];
|
|
427
|
+
const count = item?.jobs || item?.value || 0;
|
|
428
|
+
// Title is the X-axis value (month/date), use it for formatting
|
|
429
|
+
const dateValue = item?.date || title || '';
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
title: t("Planting Activities"),
|
|
433
|
+
subTitle: formatDateAxis(dateValue),
|
|
434
|
+
items: [
|
|
435
|
+
{
|
|
436
|
+
label: t("Total"),
|
|
437
|
+
value: count,
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
}}
|
|
442
|
+
formattedXAxis={formatDateAxis}
|
|
443
|
+
formattedYAxis={(value) => {
|
|
444
|
+
return `${value}`.replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => `${s},`);
|
|
445
|
+
}}
|
|
446
|
+
yAxis={{
|
|
447
|
+
min: 0,
|
|
448
|
+
max: maxActivitiesYValue,
|
|
449
|
+
tickMethod: () => {
|
|
450
|
+
// Generate ticks: for 100 max, show 0, 20, 40, 60, 80, 100
|
|
451
|
+
// For other values, show ticks every 20 units
|
|
452
|
+
const step = maxActivitiesYValue <= 100 ? 20 : Math.max(20, Math.floor(maxActivitiesYValue / 5));
|
|
453
|
+
const ticks = [];
|
|
454
|
+
for (let i = 0; i <= maxActivitiesYValue; i += step) {
|
|
455
|
+
ticks.push(i);
|
|
456
|
+
}
|
|
457
|
+
// Ensure max value is included
|
|
458
|
+
if (ticks.length === 0 || ticks[ticks.length - 1] < maxActivitiesYValue) {
|
|
459
|
+
ticks.push(maxActivitiesYValue);
|
|
460
|
+
}
|
|
461
|
+
return ticks;
|
|
462
|
+
},
|
|
463
|
+
label: {
|
|
464
|
+
style: {
|
|
465
|
+
fontSize: 12,
|
|
466
|
+
fill: '#666',
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
grid: {
|
|
470
|
+
line: {
|
|
471
|
+
style: {
|
|
472
|
+
stroke: '#E5E7EB',
|
|
473
|
+
lineWidth: 1,
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
}}
|
|
478
|
+
xAxis={{
|
|
479
|
+
label: {
|
|
480
|
+
formatter: formatDateAxis,
|
|
481
|
+
autoHide: true,
|
|
482
|
+
style: {
|
|
483
|
+
fontSize: 12,
|
|
484
|
+
fill: '#666',
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
}}
|
|
488
|
+
/>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
</Widget>
|
|
492
|
+
</section>
|
|
493
|
+
</div>
|
|
494
|
+
</Widget>
|
|
495
|
+
</section>
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
export default CycleOutcomes;
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
|
|
9
|
+
const defaultConfig = useMemo(
|
|
10
|
+
() => ({
|
|
11
|
+
basepath: "planting-cycle",
|
|
12
|
+
url: `/summary/${id}/key-information`,
|
|
13
|
+
stop: !id,
|
|
14
|
+
}),
|
|
15
|
+
[id],
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const customGetData = useMemo(() => {
|
|
19
|
+
if (getSummaryDetail && id) {
|
|
20
|
+
return ({ url, params = {} }) => {
|
|
21
|
+
const match = url.match(/\/summary\/[^/]+\/(.+)/);
|
|
22
|
+
if (match) {
|
|
23
|
+
const [, type] = match;
|
|
24
|
+
return getSummaryDetail(id, type, params);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}, [getSummaryDetail, id]);
|
|
31
|
+
|
|
32
|
+
const { loading: keyInformationLoading, data: keyInformationData } = useWidgetFetch({
|
|
33
|
+
config: defaultConfig,
|
|
34
|
+
getData: customGetData
|
|
35
|
+
});
|
|
36
|
+
const keyIndicatorsConfig = useMemo(() => getKeyIndicatorsRowConfig({ t, data: keyInformationData }), [t, keyInformationData]);
|
|
37
|
+
return (
|
|
38
|
+
<section>
|
|
39
|
+
<KeyIndicators
|
|
40
|
+
title={t("Key Information")}
|
|
41
|
+
config={keyIndicatorsConfig}
|
|
42
|
+
loading={loading || keyInformationLoading}
|
|
43
|
+
/>
|
|
44
|
+
</section>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default KeyInformation;
|
|
49
|
+
|
package/src/@daf/pages/Summary/Activities/PlantingCycle/components/PlantingLocations/index.jsx
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Widget, MineSiteMap } from '../../../../../../../../src/index.js';
|
|
3
|
+
import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
|
|
4
|
+
|
|
5
|
+
const PlantingLocations = ({
|
|
6
|
+
id,
|
|
7
|
+
getSummaryDetail,
|
|
8
|
+
loading = false,
|
|
9
|
+
t = (s) => s
|
|
10
|
+
}) => {
|
|
11
|
+
const defaultConfig = useMemo(
|
|
12
|
+
() => ({
|
|
13
|
+
basepath: "planting-cycle",
|
|
14
|
+
url: `/summary/${id}/locations`,
|
|
15
|
+
stop: !id,
|
|
16
|
+
}),
|
|
17
|
+
[id],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const customGetData = useMemo(() => {
|
|
21
|
+
if (getSummaryDetail && id) {
|
|
22
|
+
return ({ url, params = {} }) => {
|
|
23
|
+
const match = url.match(/\/summary\/[^/]+\/(.+)/);
|
|
24
|
+
if (match) {
|
|
25
|
+
const [, type] = match;
|
|
26
|
+
return getSummaryDetail(id, type, params);
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}, [getSummaryDetail, id]);
|
|
33
|
+
|
|
34
|
+
const { loading: plantingLocationsLoading, data: plantingLocationsData } = useWidgetFetch({
|
|
35
|
+
config: defaultConfig,
|
|
36
|
+
getData: customGetData
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
const mappedData = useMemo(() => {
|
|
41
|
+
if (!plantingLocationsData || !plantingLocationsData.events) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { locations = [], events } = plantingLocationsData;
|
|
46
|
+
|
|
47
|
+
// Filter events that have valid GPS coordinates
|
|
48
|
+
const eventsWithGPS = events.filter(event =>
|
|
49
|
+
event.locationCheckArrival &&
|
|
50
|
+
event.locationCheckArrival.latitude &&
|
|
51
|
+
event.locationCheckArrival.longitude
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return eventsWithGPS.map((event, index) => {
|
|
55
|
+
const locationCheckArrival = event.locationCheckArrival;
|
|
56
|
+
|
|
57
|
+
const matchingLocation = locations.find(location =>
|
|
58
|
+
locationCheckArrival.name === location.name ||
|
|
59
|
+
locationCheckArrival._id === location.id ||
|
|
60
|
+
location.id === locationCheckArrival._id
|
|
61
|
+
) || locations[0];
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const area = matchingLocation?.perimeter ? matchingLocation.perimeter.map(coord =>
|
|
65
|
+
Array.isArray(coord) && coord.length >= 2
|
|
66
|
+
? [coord[1], coord[0]]
|
|
67
|
+
: coord
|
|
68
|
+
) : [];
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const gps = {
|
|
72
|
+
latitude: locationCheckArrival.latitude,
|
|
73
|
+
longitude: locationCheckArrival.longitude
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
const color = "#15FFFFB2"
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
_id: locationCheckArrival._id || event._id || {},
|
|
81
|
+
area: area,
|
|
82
|
+
color: color,
|
|
83
|
+
datastakeId: `LOC-${String(index + 1).padStart(9, '0')}`,
|
|
84
|
+
gps: gps,
|
|
85
|
+
id: matchingLocation?.id || locationCheckArrival._id || `event-${index}`,
|
|
86
|
+
name: locationCheckArrival.name || matchingLocation?.name || `Event ${index + 1}`,
|
|
87
|
+
sources: 1,
|
|
88
|
+
subTitle: locationCheckArrival.name || matchingLocation?.name || 'Planting Location',
|
|
89
|
+
type: 'Planting Location'
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}, [plantingLocationsData]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<section>
|
|
96
|
+
<Widget
|
|
97
|
+
title={t("Planting Locations")}
|
|
98
|
+
className="no-px h-w-btn-header no-pt-body no-p-body no-pb-body"
|
|
99
|
+
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
|
100
|
+
>
|
|
101
|
+
<MineSiteMap
|
|
102
|
+
data={mappedData}
|
|
103
|
+
link
|
|
104
|
+
style={{ height: '100%', width: '100%' }}
|
|
105
|
+
maxZoom={18}
|
|
106
|
+
isSatellite={true}
|
|
107
|
+
onClickLink={() => { }}
|
|
108
|
+
onFilterChange={() => { }}
|
|
109
|
+
primaryLink
|
|
110
|
+
renderTooltip={() => { }}
|
|
111
|
+
renderTooltipTags={() => { }}
|
|
112
|
+
type="location-territory"
|
|
113
|
+
/>
|
|
114
|
+
</Widget>
|
|
115
|
+
</section>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default PlantingLocations;
|
|
120
|
+
|