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.
- package/dist/components/index.js +2656 -2476
- package/dist/hooks/index.js +72 -0
- package/dist/pages/index.js +1211 -949
- package/dist/utils/index.js +13 -0
- package/package.json +1 -1
- package/src/@daf/core/components/Dashboard/Map/ChainIcon/Markers/StakeholderMarker.js +8 -76
- package/src/@daf/core/components/Dashboard/Map/ChainIcon/index.js +116 -8
- package/src/@daf/core/components/Dashboard/Map/ChainIcon/utils.js +73 -17
- package/src/@daf/core/components/Dashboard/Map/helper.js +1 -0
- package/src/@daf/core/components/Dashboard/Map/hook.js +53 -29
- package/src/@daf/core/components/Dashboard/Map/style.js +20 -5
- package/src/@daf/hooks/useTimeFilter.js +56 -0
- package/src/@daf/hooks/useViewFormUrlParams.js +84 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/config.js +7 -13
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx +3 -1
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/JobsTimeline/index.jsx +33 -101
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/helper.js +8 -6
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/index.jsx +73 -4
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/index.jsx +1 -1
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/PlantingActivitiesTimeline.jsx +148 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/RestoredArea.jsx +150 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/index.jsx +11 -390
- package/src/@daf/pages/Summary/Activities/PlantingCycle/index.jsx +2 -2
- package/src/@daf/utils/object.js +3 -1
- package/src/@daf/utils/timeFilterUtils.js +226 -0
- package/src/hooks.js +2 -1
- package/src/utils.js +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import { getFormatDate, getTimeQuantity, processChartDateData, formatDateAxis } from '../utils/timeFilterUtils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook for time filtering functionality
|
|
6
|
+
* Provides state management and formatting functions for time-based charts
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {string} options.defaultFilter - Default time filter ('daily', 'weekly', 'monthly')
|
|
10
|
+
* @returns {Object} Time filter state and utilities
|
|
11
|
+
*/
|
|
12
|
+
export const useTimeFilter = ({ defaultFilter = 'monthly' } = {}) => {
|
|
13
|
+
const [timeFilter, setTimeFilter] = useState(defaultFilter);
|
|
14
|
+
|
|
15
|
+
// Memoized format date function bound to current timeFilter
|
|
16
|
+
const getFormatDateFn = useCallback(
|
|
17
|
+
(date, breakLine = false) => getFormatDate(date, breakLine, timeFilter),
|
|
18
|
+
[timeFilter]
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Memoized time quantity function bound to current timeFilter
|
|
22
|
+
const getTimeQuantityFn = useCallback(
|
|
23
|
+
() => getTimeQuantity(timeFilter),
|
|
24
|
+
[timeFilter]
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Memoized date axis formatter
|
|
28
|
+
const formatDateAxisFn = useMemo(
|
|
29
|
+
() => (label) => formatDateAxis(label, getFormatDateFn),
|
|
30
|
+
[getFormatDateFn]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Process chart data with current time filter
|
|
34
|
+
const processData = useCallback(
|
|
35
|
+
({ mainData, filters = {}, isCumulative = false, valueField = 'total' }) => {
|
|
36
|
+
return processChartDateData({
|
|
37
|
+
mainData,
|
|
38
|
+
timeFilter,
|
|
39
|
+
filters,
|
|
40
|
+
isCumulative,
|
|
41
|
+
valueField,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
[timeFilter]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
timeFilter,
|
|
49
|
+
setTimeFilter,
|
|
50
|
+
getFormatDate: getFormatDateFn,
|
|
51
|
+
getTimeQuantity: getTimeQuantityFn,
|
|
52
|
+
formatDateAxis: formatDateAxisFn,
|
|
53
|
+
processChartDateData: processData,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
export const useViewFormUrlParams = ({
|
|
4
|
+
params,
|
|
5
|
+
pathname,
|
|
6
|
+
search,
|
|
7
|
+
searchParams,
|
|
8
|
+
setSearchParams,
|
|
9
|
+
push,
|
|
10
|
+
}) => {
|
|
11
|
+
const [namespace, setNamespace] = useState(params.namespace);
|
|
12
|
+
const [id, setId] = useState(params.id);
|
|
13
|
+
const [group, setGroup] = useState(params.group);
|
|
14
|
+
const [subsection, setSubsection] = useState(params.subsection);
|
|
15
|
+
const [source, setSource] = useState(searchParams.get("source") || null);
|
|
16
|
+
const [version, setVersion] = useState(searchParams.get("version") || null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (
|
|
20
|
+
(id && params.id !== id) ||
|
|
21
|
+
(namespace && namespace !== params.namespace)
|
|
22
|
+
) {
|
|
23
|
+
setGroup(undefined);
|
|
24
|
+
setSubsection(undefined);
|
|
25
|
+
} else {
|
|
26
|
+
setGroup(params.group);
|
|
27
|
+
setSubsection(params.subsection);
|
|
28
|
+
}
|
|
29
|
+
setNamespace(params.namespace);
|
|
30
|
+
setId(params.id);
|
|
31
|
+
}, [params, id, namespace]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (source && version) {
|
|
35
|
+
const newParams = new URLSearchParams(searchParams);
|
|
36
|
+
newParams.set("source", source);
|
|
37
|
+
newParams.set("version", version);
|
|
38
|
+
setSearchParams(newParams);
|
|
39
|
+
}
|
|
40
|
+
}, [source, version]);
|
|
41
|
+
|
|
42
|
+
const updateSourceAndVersion = (newSource, newVersion) => {
|
|
43
|
+
const newParams = new URLSearchParams(searchParams);
|
|
44
|
+
if (newSource && newVersion) {
|
|
45
|
+
newParams.set("source", newSource);
|
|
46
|
+
newParams.set("version", newVersion);
|
|
47
|
+
} else {
|
|
48
|
+
newParams.delete("source");
|
|
49
|
+
newParams.delete("version");
|
|
50
|
+
}
|
|
51
|
+
setSearchParams(newParams);
|
|
52
|
+
setSource(newSource);
|
|
53
|
+
setVersion(newVersion);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const clearSourceAndVersion = () => {
|
|
57
|
+
updateSourceAndVersion(null, null);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const match = useMemo(
|
|
61
|
+
() => ({
|
|
62
|
+
params,
|
|
63
|
+
path: pathname,
|
|
64
|
+
}),
|
|
65
|
+
[params, pathname],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
namespace,
|
|
70
|
+
id,
|
|
71
|
+
group,
|
|
72
|
+
subsection,
|
|
73
|
+
source,
|
|
74
|
+
version,
|
|
75
|
+
params,
|
|
76
|
+
searchParams,
|
|
77
|
+
setSource,
|
|
78
|
+
setVersion,
|
|
79
|
+
updateSourceAndVersion,
|
|
80
|
+
clearSourceAndVersion,
|
|
81
|
+
match,
|
|
82
|
+
search,
|
|
83
|
+
};
|
|
84
|
+
}
|
package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { MoreMenu , CustomIcon} from "../../../../../../../index";
|
|
|
3
3
|
import { renderDateFormatted } from "../../../../../../../helpers/Forms";
|
|
4
4
|
import { Avatar, Tag } from "antd";
|
|
5
5
|
import { convertDMS } from "../../../../../../../helpers/Map";
|
|
6
|
-
|
|
6
|
+
import { findOptions, capitalize } from "../../../../../../../helpers/StringHelper.js";
|
|
7
7
|
const ACTIVITIES_TAB = 'activities';
|
|
8
8
|
const PARTNERS_TAB = 'partners';
|
|
9
9
|
const INCIDENTS_TAB = 'incidents';
|
|
@@ -17,6 +17,7 @@ export const getColumns = ({
|
|
|
17
17
|
view,
|
|
18
18
|
getRedirectLink,
|
|
19
19
|
activeTab = ACTIVITIES_TAB,
|
|
20
|
+
|
|
20
21
|
}) => {
|
|
21
22
|
const renderActions = (record, viewType = 'activities') => {
|
|
22
23
|
if (record.empty) {
|
|
@@ -163,7 +164,7 @@ export const getColumns = ({
|
|
|
163
164
|
if (all.empty) {
|
|
164
165
|
return <div className="daf-default-cell" />;
|
|
165
166
|
}
|
|
166
|
-
return <Tooltip title={value}>{
|
|
167
|
+
return <Tooltip title={value}>{capitalize(value) || '-'}</Tooltip>;
|
|
167
168
|
},
|
|
168
169
|
},
|
|
169
170
|
{
|
|
@@ -371,17 +372,10 @@ export const getColumns = ({
|
|
|
371
372
|
if (all.empty) {
|
|
372
373
|
return <div className="daf-default-cell" />;
|
|
373
374
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
country = all.country.name;
|
|
379
|
-
} else if (all.location && typeof all.location === 'object' && all.location.country) {
|
|
380
|
-
country = all.location.country;
|
|
381
|
-
} else if (all.location && typeof all.location === 'object' && all.location.name) {
|
|
382
|
-
country = all.location.name;
|
|
383
|
-
}
|
|
384
|
-
return <Tooltip title={country}>{country}</Tooltip>;
|
|
375
|
+
|
|
376
|
+
const title = findOptions(value, selectOptions?.country || []) || '-';
|
|
377
|
+
|
|
378
|
+
return <Tooltip title={title}>{title}</Tooltip>
|
|
385
379
|
},
|
|
386
380
|
},
|
|
387
381
|
{
|
package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx
CHANGED
|
@@ -45,6 +45,7 @@ const AssociatedInformation = ({
|
|
|
45
45
|
endpoint = "associated-information",
|
|
46
46
|
tabsConfig = TABS_CONFIG,
|
|
47
47
|
searchFieldsMap = getSearchFields,
|
|
48
|
+
selectOptions,
|
|
48
49
|
t = (s) => s
|
|
49
50
|
}) => {
|
|
50
51
|
const [activeTab, setActiveTab] = useState(ACTIVITIES_TAB);
|
|
@@ -104,7 +105,8 @@ const AssociatedInformation = ({
|
|
|
104
105
|
projectId,
|
|
105
106
|
navigate,
|
|
106
107
|
getRedirectLink,
|
|
107
|
-
|
|
108
|
+
selectOptions,
|
|
109
|
+
}), [t, activeTab, projectId, navigate, selectOptions]);
|
|
108
110
|
|
|
109
111
|
const tableDataSource = useMemo(() =>
|
|
110
112
|
ensureArray(associatedInformationData),
|
|
@@ -1,118 +1,38 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import dayjs from 'dayjs';
|
|
3
2
|
import { Widget, ColumnChart } from '../../../../../../../../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
|
+
];
|
|
4
11
|
|
|
5
12
|
const JobsTimeline = ({
|
|
6
13
|
dayJobsTimeline,
|
|
7
14
|
loading = false,
|
|
8
15
|
t = (s) => s
|
|
9
16
|
}) => {
|
|
17
|
+
const { timeFilter, setTimeFilter, formatDateAxis, processChartDateData } = useTimeFilter({ defaultFilter: 'monthly' });
|
|
10
18
|
|
|
11
19
|
const jobsData = Array.isArray(dayJobsTimeline)
|
|
12
20
|
? dayJobsTimeline
|
|
13
21
|
: (dayJobsTimeline?.jobsTimeline || dayJobsTimeline?.jobs || dayJobsTimeline?.timeline || []);
|
|
14
22
|
|
|
15
|
-
const formatDateAxis = useMemo(() => {
|
|
16
|
-
return (label) => {
|
|
17
|
-
if (!label) return label;
|
|
18
|
-
|
|
19
|
-
// Try to parse the date using dayjs with various formats
|
|
20
|
-
let date = dayjs(label);
|
|
21
|
-
|
|
22
|
-
// If first attempt fails, try parsing as ISO date string
|
|
23
|
-
if (!date.isValid() && typeof label === 'string') {
|
|
24
|
-
date = dayjs(label, ['YYYY-MM-DD', 'YYYY-MM', 'MMM YY', 'MMM YYYY'], true);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// If it's a valid date, format it as "Mmm YY"
|
|
28
|
-
if (date.isValid()) {
|
|
29
|
-
return date.format('MMM YY');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// If it's already in "Mmm YY" format or similar, return as is
|
|
33
|
-
// Otherwise return the original label
|
|
34
|
-
return label;
|
|
35
|
-
};
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
23
|
const jobsTimelineData = useMemo(() => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const twelveMonthsAgo = now.subtract(11, 'month'); // 11 months ago + current month = 12 months
|
|
42
|
-
|
|
43
|
-
// Create a map of existing data by month (YYYY-MM format)
|
|
44
|
-
const dataMap = new Map();
|
|
45
|
-
const dates = [];
|
|
46
|
-
|
|
47
|
-
// Process jobs data if available
|
|
48
|
-
if (jobsData && Array.isArray(jobsData) && jobsData.length > 0) {
|
|
49
|
-
jobsData.forEach((item) => {
|
|
50
|
-
if (typeof item === 'object' && item !== null && item.date) {
|
|
51
|
-
const date = dayjs(item.date);
|
|
52
|
-
if (date.isValid()) {
|
|
53
|
-
const monthKey = date.format('YYYY-MM');
|
|
54
|
-
const count = Number(item.total || item.count || item.jobs || item.value || 0) || 0;
|
|
55
|
-
dates.push(date);
|
|
56
|
-
|
|
57
|
-
// If multiple entries for same month, sum them
|
|
58
|
-
if (dataMap.has(monthKey)) {
|
|
59
|
-
dataMap.set(monthKey, {
|
|
60
|
-
...dataMap.get(monthKey),
|
|
61
|
-
jobs: dataMap.get(monthKey).jobs + count,
|
|
62
|
-
});
|
|
63
|
-
} else {
|
|
64
|
-
dataMap.set(monthKey, {
|
|
65
|
-
month: item.date,
|
|
66
|
-
jobs: count,
|
|
67
|
-
date: item.date,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
24
|
+
if (!jobsData || !Array.isArray(jobsData) || jobsData.length === 0) {
|
|
25
|
+
return [];
|
|
73
26
|
}
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const lastDataDate = sortedDates[sortedDates.length - 1].startOf('month');
|
|
84
|
-
|
|
85
|
-
// Start from the earlier of: 12 months ago, or first data date
|
|
86
|
-
minDate = twelveMonthsAgo.isBefore(firstDataDate) ? twelveMonthsAgo : firstDataDate;
|
|
87
|
-
|
|
88
|
-
// End at the later of: current month, or last data date
|
|
89
|
-
maxDate = now.isAfter(lastDataDate) ? now : lastDataDate;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Generate all months in the range
|
|
93
|
-
const result = [];
|
|
94
|
-
let currentDate = minDate.clone();
|
|
95
|
-
|
|
96
|
-
while (currentDate.isBefore(maxDate) || currentDate.isSame(maxDate, 'month')) {
|
|
97
|
-
const monthKey = currentDate.format('YYYY-MM');
|
|
98
|
-
const existingData = dataMap.get(monthKey);
|
|
99
|
-
|
|
100
|
-
if (existingData) {
|
|
101
|
-
result.push(existingData);
|
|
102
|
-
} else {
|
|
103
|
-
// Fill missing month with 0
|
|
104
|
-
result.push({
|
|
105
|
-
month: currentDate.format('YYYY-MM-DD'), // Use first day of month
|
|
106
|
-
jobs: 0,
|
|
107
|
-
date: currentDate.format('YYYY-MM-DD'),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
currentDate = currentDate.add(1, 'month');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return result;
|
|
115
|
-
}, [jobsData]);
|
|
27
|
+
|
|
28
|
+
// Process data without cumulative calculation (for jobs timeline)
|
|
29
|
+
// Try to find value in total, count, jobs, or value fields
|
|
30
|
+
return processChartDateData({
|
|
31
|
+
mainData: jobsData,
|
|
32
|
+
isCumulative: false,
|
|
33
|
+
valueField: 'total', // Will fallback to count/jobs/value if total doesn't exist
|
|
34
|
+
});
|
|
35
|
+
}, [jobsData, processChartDateData]);
|
|
116
36
|
|
|
117
37
|
const maxYValue = useMemo(() => {
|
|
118
38
|
if (!jobsTimelineData || jobsTimelineData.length === 0) {
|
|
@@ -131,10 +51,22 @@ const JobsTimeline = ({
|
|
|
131
51
|
<Widget
|
|
132
52
|
title={<div>{t("Day Jobs Timeline")}</div>}
|
|
133
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
|
+
}
|
|
134
66
|
>
|
|
135
67
|
<ColumnChart
|
|
136
68
|
data={jobsTimelineData}
|
|
137
|
-
xFieldKey="
|
|
69
|
+
xFieldKey="date"
|
|
138
70
|
yFieldKey="jobs"
|
|
139
71
|
animated={true}
|
|
140
72
|
// height={200}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const HEALTH_SAFETY_COLORS = {
|
|
2
|
-
compliant: '#
|
|
3
|
-
notCompliant: '#
|
|
2
|
+
compliant: '#016C6E',
|
|
3
|
+
notCompliant: '#F97066',
|
|
4
4
|
empty: '#D9D9D9',
|
|
5
5
|
};
|
|
6
6
|
|
|
@@ -61,8 +61,9 @@ export const calculateHealthAndSafetyPieData = (healthAndSafetyDistributionData,
|
|
|
61
61
|
const total = Object.values(healthAndSafetyDistributionData).reduce((all, val) => all + (val || 0), 0);
|
|
62
62
|
|
|
63
63
|
const labels = {
|
|
64
|
-
compliant: t("
|
|
65
|
-
notCompliant: t("Not
|
|
64
|
+
compliant: t("Available"),
|
|
65
|
+
notCompliant: t("Not available"),
|
|
66
|
+
empty: t("Not answered"),
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
return Object.keys(healthAndSafetyDistributionData).map((key) => {
|
|
@@ -103,8 +104,9 @@ export const getHealthAndSafetyTooltipChildren = (item, isEmpty, healthAndSafety
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
const labels = {
|
|
106
|
-
compliant: t("
|
|
107
|
-
notCompliant: t("Not
|
|
107
|
+
compliant: t("Available"),
|
|
108
|
+
notCompliant: t("Not available"),
|
|
109
|
+
empty: t("Not answered"),
|
|
108
110
|
};
|
|
109
111
|
|
|
110
112
|
// Filter items with values > 0
|
|
@@ -2,13 +2,83 @@ import React, { useMemo, useCallback } from 'react';
|
|
|
2
2
|
import { Widget, PieChart } from '../../../../../../../../index.js';
|
|
3
3
|
import { getHealthAndSafetyDistributionData, isHealthAndSafetyDistributionEmpty, calculateHealthAndSafetyPieData, getHealthAndSafetyTooltipChildren } from './helper';
|
|
4
4
|
import { renderTooltipJsx } from '../../../../../../../../utils';
|
|
5
|
+
import { useWidgetFetch } from '../../../../../../../hooks/useWidgetFetch.js';
|
|
5
6
|
|
|
6
7
|
const HealthAndSafety = ({
|
|
7
|
-
|
|
8
|
+
id,
|
|
9
|
+
getSummaryDetail,
|
|
8
10
|
loading = false,
|
|
9
11
|
t = (s) => s
|
|
10
12
|
}) => {
|
|
11
|
-
const
|
|
13
|
+
const defaultConfig = useMemo(
|
|
14
|
+
() => ({
|
|
15
|
+
basepath: "planting-cycle",
|
|
16
|
+
url: `/summary/${id}/filtered-piechart`,
|
|
17
|
+
filters: { field: 'duosFormed' },
|
|
18
|
+
stop: !id,
|
|
19
|
+
}),
|
|
20
|
+
[id],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const customGetData = useMemo(() => {
|
|
24
|
+
if (getSummaryDetail && id) {
|
|
25
|
+
return (rest) => {
|
|
26
|
+
const { url, filters: restFilters } = rest;
|
|
27
|
+
const match = url.match(/\/summary\/[^/]+\/(.+)/);
|
|
28
|
+
if (match) {
|
|
29
|
+
const [, type] = match;
|
|
30
|
+
// Pass filters as params for the query string
|
|
31
|
+
const params = {
|
|
32
|
+
...(restFilters || {}),
|
|
33
|
+
};
|
|
34
|
+
return getSummaryDetail(id, type, params);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}, [getSummaryDetail, id]);
|
|
41
|
+
|
|
42
|
+
const { loading: pieChartLoading, data: pieChartData } = useWidgetFetch({
|
|
43
|
+
config: defaultConfig,
|
|
44
|
+
getData: customGetData
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Process the fetched pie chart data
|
|
48
|
+
// The API returns data in format: [{count: 1, duosFormed: "null"}, {count: 1, duosFormed: "no"}, {count: 1, duosFormed: "yes"}]
|
|
49
|
+
const healthAndSafetyDistributionData = useMemo(() => {
|
|
50
|
+
if (!pieChartData) return { compliant: 0, notCompliant: 0, empty: 0 };
|
|
51
|
+
|
|
52
|
+
// If it's already a distribution object
|
|
53
|
+
if (pieChartData.compliant !== undefined || pieChartData.notCompliant !== undefined) {
|
|
54
|
+
return pieChartData;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If it's an array, process it
|
|
58
|
+
if (Array.isArray(pieChartData)) {
|
|
59
|
+
const distribution = { compliant: 0, notCompliant: 0, empty: 0 };
|
|
60
|
+
|
|
61
|
+
pieChartData.forEach(item => {
|
|
62
|
+
const duosFormedValue = item.duosFormed;
|
|
63
|
+
const count = item.count || 0;
|
|
64
|
+
|
|
65
|
+
// Map duosFormed values to distribution categories
|
|
66
|
+
if (duosFormedValue === "yes" || duosFormedValue === true) {
|
|
67
|
+
distribution.compliant += count;
|
|
68
|
+
} else if (duosFormedValue === "no" || duosFormedValue === false) {
|
|
69
|
+
distribution.notCompliant += count;
|
|
70
|
+
} else if (duosFormedValue === "null" || duosFormedValue === null || duosFormedValue === undefined) {
|
|
71
|
+
distribution.empty += count;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return distribution;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fallback: try to extract from activityData-like structure
|
|
79
|
+
return getHealthAndSafetyDistributionData(pieChartData);
|
|
80
|
+
}, [pieChartData]);
|
|
81
|
+
|
|
12
82
|
const isEmpty = useMemo(() => isHealthAndSafetyDistributionEmpty(healthAndSafetyDistributionData), [healthAndSafetyDistributionData]);
|
|
13
83
|
const pieData = useMemo(() => calculateHealthAndSafetyPieData(healthAndSafetyDistributionData, t), [healthAndSafetyDistributionData, t]);
|
|
14
84
|
|
|
@@ -19,7 +89,7 @@ const HealthAndSafety = ({
|
|
|
19
89
|
|
|
20
90
|
return (
|
|
21
91
|
<Widget
|
|
22
|
-
loading={loading}
|
|
92
|
+
loading={loading || pieChartLoading}
|
|
23
93
|
title={<div>{t("Health and Safety")}</div>}
|
|
24
94
|
className="with-border-header h-w-btn-header "
|
|
25
95
|
>
|
|
@@ -46,4 +116,3 @@ const HealthAndSafety = ({
|
|
|
46
116
|
};
|
|
47
117
|
|
|
48
118
|
export default HealthAndSafety;
|
|
49
|
-
|
package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/index.jsx
CHANGED
|
@@ -100,7 +100,7 @@ const CycleIndicators = ({
|
|
|
100
100
|
<CyclePartners cyclePartners={cyclePartners} loading={indicatorsLoading} t={t} />
|
|
101
101
|
</section>
|
|
102
102
|
<section style={{ flex: 1 }}>
|
|
103
|
-
<HealthAndSafety
|
|
103
|
+
<HealthAndSafety id={id} getSummaryDetail={getSummaryDetail} loading={indicatorsLoading} t={t} />
|
|
104
104
|
</section>
|
|
105
105
|
</div>
|
|
106
106
|
</Widget>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Widget, ColumnChart } 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 PlantingActivitiesTimeline = ({
|
|
13
|
+
activitiesTimelineChart,
|
|
14
|
+
t = (s) => s
|
|
15
|
+
}) => {
|
|
16
|
+
const { timeFilter, setTimeFilter, formatDateAxis, processChartDateData } = useTimeFilter({ defaultFilter: 'monthly' });
|
|
17
|
+
|
|
18
|
+
// Map activitiesTimelineChart data to ColumnChart format with time filter support
|
|
19
|
+
// Data structure: [{count: 2, date: "2025-11-03"}]
|
|
20
|
+
// Fill all periods in the range, even if empty
|
|
21
|
+
const activitiesTimelineData = useMemo(() => {
|
|
22
|
+
if (!activitiesTimelineChart || !Array.isArray(activitiesTimelineChart) || activitiesTimelineChart.length === 0) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Process data without cumulative calculation (for activities timeline)
|
|
27
|
+
return processChartDateData({
|
|
28
|
+
mainData: activitiesTimelineChart,
|
|
29
|
+
isCumulative: false,
|
|
30
|
+
valueField: 'count',
|
|
31
|
+
});
|
|
32
|
+
}, [activitiesTimelineChart, processChartDateData]);
|
|
33
|
+
|
|
34
|
+
// Calculate max value for Y-axis (default to 100 if all values are 0 or very small)
|
|
35
|
+
const maxActivitiesYValue = useMemo(() => {
|
|
36
|
+
if (!activitiesTimelineData || activitiesTimelineData.length === 0) {
|
|
37
|
+
return 100;
|
|
38
|
+
}
|
|
39
|
+
const maxValue = Math.max(...activitiesTimelineData.map(item => item.jobs || 0));
|
|
40
|
+
// If max is 0, set default to 100 to show Y-axis
|
|
41
|
+
if (maxValue === 0) {
|
|
42
|
+
return 100;
|
|
43
|
+
}
|
|
44
|
+
// Round up to nearest 10, but ensure minimum of 100
|
|
45
|
+
const roundedMax = Math.ceil(maxValue / 10) * 10;
|
|
46
|
+
return Math.max(100, roundedMax);
|
|
47
|
+
}, [activitiesTimelineData]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Widget
|
|
51
|
+
title={t("Planting Activities Timeline")}
|
|
52
|
+
className="with-border-header h-w-btn-header"
|
|
53
|
+
addedHeader={
|
|
54
|
+
<>
|
|
55
|
+
<div className="flex-1" />
|
|
56
|
+
<Select
|
|
57
|
+
value={timeFilter}
|
|
58
|
+
style={{ width: 100 }}
|
|
59
|
+
onChange={(value) => setTimeFilter(value)}
|
|
60
|
+
options={selectOptions}
|
|
61
|
+
popupMatchSelectWidth={120}
|
|
62
|
+
/>
|
|
63
|
+
</>
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
<div className="flex flex-1 flex-column justify-content-center">
|
|
67
|
+
<div className="flex justify-content-center w-full">
|
|
68
|
+
<ColumnChart
|
|
69
|
+
data={activitiesTimelineData}
|
|
70
|
+
xFieldKey="date"
|
|
71
|
+
yFieldKey="jobs"
|
|
72
|
+
animated={true}
|
|
73
|
+
height={400}
|
|
74
|
+
color="#016C6E"
|
|
75
|
+
renderTooltipContent={(title, data) => {
|
|
76
|
+
if (!data || data.length === 0) return {};
|
|
77
|
+
// For ColumnChart, data structure: data[0]?.data contains the actual data point
|
|
78
|
+
const item = data[0]?.data || data[0];
|
|
79
|
+
const count = item?.jobs || item?.value || 0;
|
|
80
|
+
// Title is the X-axis value (month/date), use it for formatting
|
|
81
|
+
const dateValue = item?.date || title || '';
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
title: t("Planting Activities"),
|
|
85
|
+
subTitle: formatDateAxis(dateValue),
|
|
86
|
+
items: [
|
|
87
|
+
{
|
|
88
|
+
label: t("Total"),
|
|
89
|
+
value: count,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}}
|
|
94
|
+
formattedXAxis={formatDateAxis}
|
|
95
|
+
formattedYAxis={(value) => {
|
|
96
|
+
return `${value}`.replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => `${s},`);
|
|
97
|
+
}}
|
|
98
|
+
yAxis={{
|
|
99
|
+
min: 0,
|
|
100
|
+
max: maxActivitiesYValue,
|
|
101
|
+
tickMethod: () => {
|
|
102
|
+
// Generate ticks: for 100 max, show 0, 20, 40, 60, 80, 100
|
|
103
|
+
// For other values, show ticks every 20 units
|
|
104
|
+
const step = maxActivitiesYValue <= 100 ? 20 : Math.max(20, Math.floor(maxActivitiesYValue / 5));
|
|
105
|
+
const ticks = [];
|
|
106
|
+
for (let i = 0; i <= maxActivitiesYValue; i += step) {
|
|
107
|
+
ticks.push(i);
|
|
108
|
+
}
|
|
109
|
+
// Ensure max value is included
|
|
110
|
+
if (ticks.length === 0 || ticks[ticks.length - 1] < maxActivitiesYValue) {
|
|
111
|
+
ticks.push(maxActivitiesYValue);
|
|
112
|
+
}
|
|
113
|
+
return ticks;
|
|
114
|
+
},
|
|
115
|
+
label: {
|
|
116
|
+
style: {
|
|
117
|
+
fontSize: 12,
|
|
118
|
+
fill: '#666',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
grid: {
|
|
122
|
+
line: {
|
|
123
|
+
style: {
|
|
124
|
+
stroke: '#E5E7EB',
|
|
125
|
+
lineWidth: 1,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
}}
|
|
130
|
+
xAxis={{
|
|
131
|
+
label: {
|
|
132
|
+
formatter: formatDateAxis,
|
|
133
|
+
autoHide: true,
|
|
134
|
+
style: {
|
|
135
|
+
fontSize: 12,
|
|
136
|
+
fill: '#666',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</Widget>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default PlantingActivitiesTimeline;
|
|
148
|
+
|