datastake-daf 0.6.774 → 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 +2384 -2252
- package/dist/pages/index.js +1802 -820
- package/dist/style/datastake/mapbox-gl.css +330 -0
- package/package.json +1 -1
- package/src/@daf/hooks/useTimeFilter.js +56 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/config.js +548 -0
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx +137 -24
- package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/JobsTimeline/index.jsx +33 -102
- 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 +3 -4
- package/src/@daf/utils/timeFilterUtils.js +226 -0
package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx
CHANGED
|
@@ -1,40 +1,153 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Widget } from '../../../../../../../../src/index.js';
|
|
1
|
+
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Widget, StickyTable, SearchFilters } from '../../../../../../../../src/index.js';
|
|
3
|
+
import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
|
|
4
|
+
import { getColumns } from './config.js';
|
|
5
|
+
import { getRedirectLink } from '../../../../../../../utils.js';
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
+
// Constants
|
|
8
|
+
export const ACTIVITIES_TAB = 'activities';
|
|
9
|
+
export const PARTNERS_TAB = 'partners';
|
|
10
|
+
export const INCIDENTS_TAB = 'incidents';
|
|
7
11
|
|
|
12
|
+
export const DEFAULT_SEARCH_FIELDS = ["name", "datastakeId"];
|
|
13
|
+
const URL_PATTERN = /\/summary\/[^/]+\/(.+)/;
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
// Configuration
|
|
16
|
+
export const TABS_CONFIG = [
|
|
17
|
+
{ label: "straatos::activities", value: ACTIVITIES_TAB },
|
|
18
|
+
{ label: "straatos::partners", value: PARTNERS_TAB },
|
|
19
|
+
{ label: "straatos::incidents", value: INCIDENTS_TAB, disabled: true },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Helper functions
|
|
23
|
+
export const getSearchFields = (activeTab) => DEFAULT_SEARCH_FIELDS;
|
|
24
|
+
|
|
25
|
+
export const buildSearchFilter = (search, fields) =>
|
|
26
|
+
search ? { search: { qs: search, fields } } : {};
|
|
27
|
+
|
|
28
|
+
export const extractTypeFromUrl = (url) => {
|
|
29
|
+
const match = url.match(URL_PATTERN);
|
|
30
|
+
if (!match) {
|
|
31
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
32
|
+
}
|
|
33
|
+
return match[1];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const ensureArray = (data) => Array.isArray(data) ? data : [];
|
|
37
|
+
|
|
38
|
+
const AssociatedInformation = ({
|
|
39
|
+
id,
|
|
40
|
+
navigate,
|
|
41
|
+
getSummaryDetail,
|
|
42
|
+
loading = false,
|
|
43
|
+
projectId,
|
|
44
|
+
basepath = "planting-cycle",
|
|
45
|
+
endpoint = "associated-information",
|
|
46
|
+
tabsConfig = TABS_CONFIG,
|
|
47
|
+
searchFieldsMap = getSearchFields,
|
|
48
|
+
selectOptions,
|
|
49
|
+
t = (s) => s
|
|
13
50
|
}) => {
|
|
14
51
|
const [activeTab, setActiveTab] = useState(ACTIVITIES_TAB);
|
|
52
|
+
const [search, setSearch] = useState('');
|
|
53
|
+
|
|
54
|
+
const searchFields = useMemo(() => searchFieldsMap(activeTab), [activeTab, searchFieldsMap]);
|
|
55
|
+
|
|
56
|
+
const filters = useMemo(() => ({
|
|
57
|
+
type: activeTab,
|
|
58
|
+
...buildSearchFilter(search, searchFields)
|
|
59
|
+
}), [activeTab, search, searchFields]);
|
|
60
|
+
|
|
61
|
+
const defaultConfig = useMemo(() => ({
|
|
62
|
+
basepath,
|
|
63
|
+
url: `/summary/${id}/${endpoint}`,
|
|
64
|
+
filters,
|
|
65
|
+
stop: !id,
|
|
66
|
+
}), [id, filters, basepath, endpoint]);
|
|
67
|
+
|
|
68
|
+
const customGetData = useMemo(() => {
|
|
69
|
+
if (!getSummaryDetail || !id) return undefined;
|
|
70
|
+
|
|
71
|
+
return (rest) => {
|
|
72
|
+
const { url, filters: restFilters } = rest;
|
|
73
|
+
const type = extractTypeFromUrl(url);
|
|
74
|
+
const params = {
|
|
75
|
+
...(restFilters || {}),
|
|
76
|
+
type: restFilters?.type || activeTab
|
|
77
|
+
};
|
|
78
|
+
return getSummaryDetail(id, type, params);
|
|
79
|
+
};
|
|
80
|
+
}, [getSummaryDetail, id, activeTab]);
|
|
81
|
+
|
|
82
|
+
const { loading: associatedInformationLoading, data: associatedInformationData, setData } = useWidgetFetch({
|
|
83
|
+
config: defaultConfig,
|
|
84
|
+
getData: customGetData
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Reset data and search when tab changes
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
setData([]);
|
|
90
|
+
setSearch('');
|
|
91
|
+
}, [activeTab, setData]);
|
|
92
|
+
|
|
93
|
+
const handleSearch = useCallback((activeFilter, searchValue) => {
|
|
94
|
+
setSearch(searchValue || '');
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const handleTabChange = useCallback((value) => {
|
|
98
|
+
setActiveTab(value);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const columns = useMemo(() => getColumns({
|
|
102
|
+
t,
|
|
103
|
+
activeTab,
|
|
104
|
+
view: activeTab,
|
|
105
|
+
projectId,
|
|
106
|
+
navigate,
|
|
107
|
+
getRedirectLink,
|
|
108
|
+
selectOptions,
|
|
109
|
+
}), [t, activeTab, projectId, navigate, selectOptions]);
|
|
110
|
+
|
|
111
|
+
const tableDataSource = useMemo(() =>
|
|
112
|
+
ensureArray(associatedInformationData),
|
|
113
|
+
[associatedInformationData]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const translatedTabs = useMemo(() =>
|
|
117
|
+
tabsConfig.map(tab => ({ ...tab, label: t(tab.label) })),
|
|
118
|
+
[tabsConfig, t]
|
|
119
|
+
);
|
|
15
120
|
|
|
16
121
|
return (
|
|
17
122
|
<section>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
className="v2-widget no-px h-w-btn-header no-p-body"
|
|
123
|
+
<Widget
|
|
124
|
+
className="v2-widget no-px no-p-body h-w-btn-header with-border-header"
|
|
21
125
|
title={t("Associated Information")}
|
|
22
126
|
tabsConfig={{
|
|
23
|
-
tabs:
|
|
24
|
-
{ label: t("straatos::activities"), value: ACTIVITIES_TAB },
|
|
25
|
-
{ label: t("straatos::partners"), value: PARTNERS_TAB },
|
|
26
|
-
{ label: t("straatos::incidents"), value: INCIDENTS_TAB },
|
|
27
|
-
],
|
|
127
|
+
tabs: translatedTabs,
|
|
28
128
|
value: activeTab,
|
|
29
|
-
onChange:
|
|
30
|
-
setActiveTab(value);
|
|
31
|
-
// setData([]);
|
|
32
|
-
},
|
|
129
|
+
onChange: handleTabChange,
|
|
33
130
|
}}
|
|
34
131
|
>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
132
|
+
<div className='mt-6 ml-6 mr-6'>
|
|
133
|
+
<SearchFilters
|
|
134
|
+
t={t}
|
|
135
|
+
showFilter={false}
|
|
136
|
+
hasError={false}
|
|
137
|
+
canClear={true}
|
|
138
|
+
setHasError={() => {}}
|
|
139
|
+
onSearch={handleSearch}
|
|
140
|
+
activeFilters={{ search }}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
<div className='mb-6'>
|
|
144
|
+
<StickyTable
|
|
145
|
+
columns={columns}
|
|
146
|
+
dataSource={tableDataSource}
|
|
147
|
+
loading={associatedInformationLoading || loading}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</Widget>
|
|
38
151
|
</section>
|
|
39
152
|
);
|
|
40
153
|
};
|
|
@@ -1,119 +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
|
-
console.log('dayJobsTimeline', dayJobsTimeline);
|
|
12
19
|
const jobsData = Array.isArray(dayJobsTimeline)
|
|
13
20
|
? dayJobsTimeline
|
|
14
21
|
: (dayJobsTimeline?.jobsTimeline || dayJobsTimeline?.jobs || dayJobsTimeline?.timeline || []);
|
|
15
22
|
|
|
16
|
-
const formatDateAxis = useMemo(() => {
|
|
17
|
-
return (label) => {
|
|
18
|
-
if (!label) return label;
|
|
19
|
-
|
|
20
|
-
// Try to parse the date using dayjs with various formats
|
|
21
|
-
let date = dayjs(label);
|
|
22
|
-
|
|
23
|
-
// If first attempt fails, try parsing as ISO date string
|
|
24
|
-
if (!date.isValid() && typeof label === 'string') {
|
|
25
|
-
date = dayjs(label, ['YYYY-MM-DD', 'YYYY-MM', 'MMM YY', 'MMM YYYY'], true);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// If it's a valid date, format it as "Mmm YY"
|
|
29
|
-
if (date.isValid()) {
|
|
30
|
-
return date.format('MMM YY');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// If it's already in "Mmm YY" format or similar, return as is
|
|
34
|
-
// Otherwise return the original label
|
|
35
|
-
return label;
|
|
36
|
-
};
|
|
37
|
-
}, []);
|
|
38
|
-
|
|
39
23
|
const jobsTimelineData = useMemo(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const twelveMonthsAgo = now.subtract(11, 'month'); // 11 months ago + current month = 12 months
|
|
43
|
-
|
|
44
|
-
// Create a map of existing data by month (YYYY-MM format)
|
|
45
|
-
const dataMap = new Map();
|
|
46
|
-
const dates = [];
|
|
47
|
-
|
|
48
|
-
// Process jobs data if available
|
|
49
|
-
if (jobsData && Array.isArray(jobsData) && jobsData.length > 0) {
|
|
50
|
-
jobsData.forEach((item) => {
|
|
51
|
-
if (typeof item === 'object' && item !== null && item.date) {
|
|
52
|
-
const date = dayjs(item.date);
|
|
53
|
-
if (date.isValid()) {
|
|
54
|
-
const monthKey = date.format('YYYY-MM');
|
|
55
|
-
const count = Number(item.total || item.count || item.jobs || item.value || 0) || 0;
|
|
56
|
-
dates.push(date);
|
|
57
|
-
|
|
58
|
-
// If multiple entries for same month, sum them
|
|
59
|
-
if (dataMap.has(monthKey)) {
|
|
60
|
-
dataMap.set(monthKey, {
|
|
61
|
-
...dataMap.get(monthKey),
|
|
62
|
-
jobs: dataMap.get(monthKey).jobs + count,
|
|
63
|
-
});
|
|
64
|
-
} else {
|
|
65
|
-
dataMap.set(monthKey, {
|
|
66
|
-
month: item.date,
|
|
67
|
-
jobs: count,
|
|
68
|
-
date: item.date,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
24
|
+
if (!jobsData || !Array.isArray(jobsData) || jobsData.length === 0) {
|
|
25
|
+
return [];
|
|
74
26
|
}
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const lastDataDate = sortedDates[sortedDates.length - 1].startOf('month');
|
|
85
|
-
|
|
86
|
-
// Start from the earlier of: 12 months ago, or first data date
|
|
87
|
-
minDate = twelveMonthsAgo.isBefore(firstDataDate) ? twelveMonthsAgo : firstDataDate;
|
|
88
|
-
|
|
89
|
-
// End at the later of: current month, or last data date
|
|
90
|
-
maxDate = now.isAfter(lastDataDate) ? now : lastDataDate;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Generate all months in the range
|
|
94
|
-
const result = [];
|
|
95
|
-
let currentDate = minDate.clone();
|
|
96
|
-
|
|
97
|
-
while (currentDate.isBefore(maxDate) || currentDate.isSame(maxDate, 'month')) {
|
|
98
|
-
const monthKey = currentDate.format('YYYY-MM');
|
|
99
|
-
const existingData = dataMap.get(monthKey);
|
|
100
|
-
|
|
101
|
-
if (existingData) {
|
|
102
|
-
result.push(existingData);
|
|
103
|
-
} else {
|
|
104
|
-
// Fill missing month with 0
|
|
105
|
-
result.push({
|
|
106
|
-
month: currentDate.format('YYYY-MM-DD'), // Use first day of month
|
|
107
|
-
jobs: 0,
|
|
108
|
-
date: currentDate.format('YYYY-MM-DD'),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
currentDate = currentDate.add(1, 'month');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return result;
|
|
116
|
-
}, [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]);
|
|
117
36
|
|
|
118
37
|
const maxYValue = useMemo(() => {
|
|
119
38
|
if (!jobsTimelineData || jobsTimelineData.length === 0) {
|
|
@@ -132,10 +51,22 @@ const JobsTimeline = ({
|
|
|
132
51
|
<Widget
|
|
133
52
|
title={<div>{t("Day Jobs Timeline")}</div>}
|
|
134
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
|
+
}
|
|
135
66
|
>
|
|
136
67
|
<ColumnChart
|
|
137
68
|
data={jobsTimelineData}
|
|
138
|
-
xFieldKey="
|
|
69
|
+
xFieldKey="date"
|
|
139
70
|
yFieldKey="jobs"
|
|
140
71
|
animated={true}
|
|
141
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
|
+
|