datastake-daf 0.6.773 → 0.6.775

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 (27) hide show
  1. package/dist/components/index.js +2656 -2476
  2. package/dist/hooks/index.js +72 -0
  3. package/dist/pages/index.js +1211 -949
  4. package/dist/utils/index.js +13 -0
  5. package/package.json +1 -1
  6. package/src/@daf/core/components/Dashboard/Map/ChainIcon/Markers/StakeholderMarker.js +8 -76
  7. package/src/@daf/core/components/Dashboard/Map/ChainIcon/index.js +116 -8
  8. package/src/@daf/core/components/Dashboard/Map/ChainIcon/utils.js +73 -17
  9. package/src/@daf/core/components/Dashboard/Map/helper.js +1 -0
  10. package/src/@daf/core/components/Dashboard/Map/hook.js +53 -29
  11. package/src/@daf/core/components/Dashboard/Map/style.js +20 -5
  12. package/src/@daf/hooks/useTimeFilter.js +56 -0
  13. package/src/@daf/hooks/useViewFormUrlParams.js +84 -0
  14. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/config.js +7 -13
  15. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx +3 -1
  16. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/JobsTimeline/index.jsx +33 -101
  17. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/helper.js +8 -6
  18. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/index.jsx +73 -4
  19. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/index.jsx +1 -1
  20. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/PlantingActivitiesTimeline.jsx +148 -0
  21. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/RestoredArea.jsx +150 -0
  22. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/index.jsx +11 -390
  23. package/src/@daf/pages/Summary/Activities/PlantingCycle/index.jsx +2 -2
  24. package/src/@daf/utils/object.js +3 -1
  25. package/src/@daf/utils/timeFilterUtils.js +226 -0
  26. package/src/hooks.js +2 -1
  27. package/src/utils.js +1 -1
@@ -0,0 +1,150 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Widget, LineChart } from '../../../../../../../../src/index.js';
3
+ import { Select } from 'antd';
4
+ import { useTimeFilter } from '../../../../../../hooks/useTimeFilter.js';
5
+
6
+ const selectOptions = [
7
+ { label: "Daily", value: "daily" },
8
+ { label: "Weekly", value: "weekly" },
9
+ { label: "Monthly", value: "monthly" },
10
+ ];
11
+
12
+ const RestoredArea = ({
13
+ restoredAreaChart,
14
+ t = (s) => s
15
+ }) => {
16
+ const { timeFilter, setTimeFilter, formatDateAxis, processChartDateData } = useTimeFilter({ defaultFilter: 'monthly' });
17
+
18
+ // Map restoredAreaChart data to LineChart format with time filter support
19
+ // Y-axis: total/cumulated value
20
+ // X-axis: date (formatted based on timeFilter)
21
+ // Fill all periods in the range, even if empty
22
+ const restoredAreaChartData = useMemo(() => {
23
+ if (!restoredAreaChart || !Array.isArray(restoredAreaChart) || restoredAreaChart.length === 0) {
24
+ return [];
25
+ }
26
+
27
+ // Process data with cumulative calculation (for restored area)
28
+ return processChartDateData({
29
+ mainData: restoredAreaChart,
30
+ isCumulative: true,
31
+ valueField: 'total',
32
+ });
33
+ }, [restoredAreaChart, processChartDateData]);
34
+
35
+ // Calculate max value for yAxis dynamically (round up to nearest 10)
36
+ const maxYValue = useMemo(() => {
37
+ if (!restoredAreaChartData || restoredAreaChartData.length === 0) {
38
+ return 80;
39
+ }
40
+ const maxValue = Math.max(...restoredAreaChartData.map(item => item.value || 0));
41
+ // If max is 0, set default to 80 to show Y-axis
42
+ // Otherwise, round up to nearest 10
43
+ if (maxValue === 0) {
44
+ return 80;
45
+ }
46
+ return Math.ceil(maxValue / 10) * 10 || 80;
47
+ }, [restoredAreaChartData]);
48
+
49
+
50
+ return (
51
+ <Widget
52
+ title={t("Restored Area")}
53
+ className="with-border-header h-w-btn-header"
54
+ addedHeader={
55
+ <>
56
+ <div className="flex-1" />
57
+ <Select
58
+ value={timeFilter}
59
+ style={{ width: 100 }}
60
+ onChange={(value) => setTimeFilter(value)}
61
+ options={selectOptions}
62
+ popupMatchSelectWidth={120}
63
+ />
64
+ </>
65
+ }
66
+ >
67
+ <div className="flex flex-1 flex-column justify-content-center">
68
+ <div className="flex justify-content-center w-full">
69
+ <LineChart
70
+ animated
71
+ isArea
72
+ color={'#00AEB1'}
73
+ data={restoredAreaChartData}
74
+ fillOpacity={0.7}
75
+ height={200}
76
+ renderTooltipContent={(title, data) => {
77
+ if (!data || data.length === 0) return {};
78
+ const item = data[0]?.data || data[0];
79
+ const periodValue = item?.period !== undefined ? item.period : (item?.value || 0);
80
+ const cumulatedValue = item?.value || 0;
81
+
82
+ return {
83
+ title: t("Restored Area"),
84
+ subTitle: formatDateAxis(title),
85
+ items: [
86
+ {
87
+ label: t("Period"),
88
+ value: `${periodValue.toLocaleString()} ha`,
89
+ },
90
+ {
91
+ label: t("Cumulated"),
92
+ value: `${cumulatedValue.toLocaleString()} ha`,
93
+ },
94
+ ],
95
+ };
96
+ }}
97
+ xFieldKey="date"
98
+ yFieldKey="value"
99
+ formattedXAxis={formatDateAxis}
100
+ style={{ width: '100%' }}
101
+ yAxis={{
102
+ min: 0,
103
+ max: maxYValue,
104
+ tickMethod: () => {
105
+ // Generate ticks every 10 units for maxYValue of 80
106
+ // For other values, show ticks every 10 units
107
+ const step = maxYValue <= 80 ? 10 : Math.max(10, Math.floor(maxYValue / 8));
108
+ const ticks = [];
109
+ for (let i = 0; i <= maxYValue; i += step) {
110
+ ticks.push(i);
111
+ }
112
+ // Ensure max value is included
113
+ if (ticks.length === 0 || ticks[ticks.length - 1] < maxYValue) {
114
+ ticks.push(maxYValue);
115
+ }
116
+ return ticks;
117
+ },
118
+ label: {
119
+ style: {
120
+ fontSize: 12,
121
+ fill: '#666',
122
+ },
123
+ },
124
+ grid: {
125
+ line: {
126
+ style: {
127
+ stroke: '#E5E7EB',
128
+ lineWidth: 1,
129
+ },
130
+ },
131
+ },
132
+ }}
133
+ xAxis={{
134
+ label: {
135
+ formatter: formatDateAxis,
136
+ style: {
137
+ fontSize: 12,
138
+ fill: '#666',
139
+ },
140
+ },
141
+ }}
142
+ />
143
+ </div>
144
+ </div>
145
+ </Widget>
146
+ );
147
+ };
148
+
149
+ export default RestoredArea;
150
+
@@ -1,8 +1,9 @@
1
1
  import React, { useMemo } from 'react';
2
- import dayjs from 'dayjs';
3
- import { Widget, StatCard, LineChart, ColumnChart } from '../../../../../../../../src/index.js';
2
+ import { Widget, StatCard } from '../../../../../../../../src/index.js';
4
3
  import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
5
4
  import { calculateStatChange } from '../../helper.js';
5
+ import RestoredArea from './RestoredArea.jsx';
6
+ import PlantingActivitiesTimeline from './PlantingActivitiesTimeline.jsx';
6
7
 
7
8
  const CycleOutcomes = ({
8
9
  id,
@@ -11,7 +12,6 @@ const CycleOutcomes = ({
11
12
  t = (s) => s
12
13
  }) => {
13
14
 
14
-
15
15
  const defaultConfig = useMemo(
16
16
  () => ({
17
17
  basepath: "planting-cycle",
@@ -71,228 +71,6 @@ const CycleOutcomes = ({
71
71
  );
72
72
  }, [locationsCount, t]);
73
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
74
  return (
297
75
  <section>
298
76
  <Widget
@@ -322,173 +100,16 @@ const CycleOutcomes = ({
322
100
 
323
101
  <div style={{ display: "flex", gap: "24px" }}>
324
102
  <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>
103
+ <RestoredArea
104
+ restoredAreaChart={restoredAreaChart}
105
+ t={t}
106
+ />
408
107
  </section>
409
108
  <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>
109
+ <PlantingActivitiesTimeline
110
+ activitiesTimelineChart={activitiesTimelineChart}
111
+ t={t}
112
+ />
492
113
  </section>
493
114
  </div>
494
115
  </Widget>
@@ -6,7 +6,7 @@ import CommunityParticipation from './components/CommunityParticipation/index.js
6
6
  import AssociatedInformation from './components/AssociatedInformation/index.jsx';
7
7
  import KeyInformation from './components/KeyInformation/index.jsx';
8
8
 
9
- const PlantingCycleSummary = ({ header, activityData, loading = false, id, projectId, t = () => { }, getSummaryDetail, navigate }) => {
9
+ const PlantingCycleSummary = ({ header, activityData, loading = false, id, projectId, t = () => { }, getSummaryDetail, navigate, selectOptions }) => {
10
10
  return (
11
11
  <DashboardLayout
12
12
  header={
@@ -27,7 +27,7 @@ const PlantingCycleSummary = ({ header, activityData, loading = false, id, proje
27
27
  <CycleOutcomes id={id} getSummaryDetail={getSummaryDetail} loading={loading} t={t} />
28
28
  <CycleIndicators id={id} getSummaryDetail={getSummaryDetail} loading={loading} t={t} />
29
29
  <CommunityParticipation id={id} getSummaryDetail={getSummaryDetail} loading={loading} t={t} />
30
- <AssociatedInformation id={id} projectId={projectId} getSummaryDetail={getSummaryDetail} loading={loading} t={t} navigate={navigate} />
30
+ <AssociatedInformation id={id} projectId={projectId} getSummaryDetail={getSummaryDetail} loading={loading} t={t} navigate={navigate} selectOptions={selectOptions} />
31
31
  </DashboardLayout>
32
32
  )
33
33
  }
@@ -61,4 +61,6 @@ export const removeKeysFromObject = (obj = {}, keys = []) => {
61
61
  }
62
62
  }
63
63
  return result;
64
- }
64
+ }
65
+
66
+ export const hasKeyInObject = (obj, key) => Object.keys(obj || {}).includes(key);