datastake-daf 0.6.768 → 0.6.770
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 +792 -580
- 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,60 @@
|
|
|
1
|
+
import { calculateStatChange } from '../../../helper.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculates the change for day jobs stat
|
|
5
|
+
* @param {Object} dayJobs - Object with current and previous day jobs values
|
|
6
|
+
* @param {Function} t - Translation function
|
|
7
|
+
* @returns {Object|null} Change object for StatCard or null if data is invalid
|
|
8
|
+
*/
|
|
9
|
+
export const calculateDayJobsChange = (dayJobs, t) => {
|
|
10
|
+
if (!dayJobs) return null;
|
|
11
|
+
return calculateStatChange(
|
|
12
|
+
{
|
|
13
|
+
current: Number(dayJobs.current) || 0,
|
|
14
|
+
previous: Number(dayJobs.previous) || 0,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
tooltipText: t("In comparison to last cycle"),
|
|
18
|
+
format: 'absolute',
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Calculates the change for employed women stat
|
|
25
|
+
* @param {Object} employedWomen - Object with current and previous employed women values
|
|
26
|
+
* @param {Function} t - Translation function
|
|
27
|
+
* @returns {Object|null} Change object for StatCard or null if data is invalid
|
|
28
|
+
*/
|
|
29
|
+
export const calculateEmployedWomenChange = (employedWomen, t) => {
|
|
30
|
+
if (!employedWomen) return null;
|
|
31
|
+
return calculateStatChange(
|
|
32
|
+
{
|
|
33
|
+
current: Number(employedWomen.current) || 0,
|
|
34
|
+
previous: Number(employedWomen.previous) || 0,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
tooltipText: t("In comparison to last cycle"),
|
|
38
|
+
format: 'absolute',
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Formats the day jobs value for display
|
|
45
|
+
* @param {Object} dayJobs - Object with current day jobs value
|
|
46
|
+
* @returns {string} Formatted value string
|
|
47
|
+
*/
|
|
48
|
+
export const formatDayJobsValue = (dayJobs) => {
|
|
49
|
+
return dayJobs?.current ? Number(dayJobs.current).toLocaleString() : 0;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Formats the employed women value for display
|
|
54
|
+
* @param {Object} employedWomen - Object with current employed women value
|
|
55
|
+
* @returns {string} Formatted value string
|
|
56
|
+
*/
|
|
57
|
+
export const formatEmployedWomenValue = (employedWomen) => {
|
|
58
|
+
return employedWomen?.current ? Number(employedWomen.current).toLocaleString() : 0;
|
|
59
|
+
};
|
|
60
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { StatCard } from '../../../../../../../../index.js';
|
|
3
|
+
import { calculateDayJobsChange, calculateEmployedWomenChange, formatDayJobsValue, formatEmployedWomenValue } from './helper.js';
|
|
4
|
+
|
|
5
|
+
const CommunityStats = ({
|
|
6
|
+
dayJobs,
|
|
7
|
+
employedWomen,
|
|
8
|
+
t = (s) => s
|
|
9
|
+
}) => {
|
|
10
|
+
const dayJobsChange = useMemo(() => calculateDayJobsChange(dayJobs, t), [dayJobs, t]);
|
|
11
|
+
const employedWomenChange = useMemo(() => calculateEmployedWomenChange(employedWomen, t), [employedWomen, t]);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div style={{ display: 'flex', gap: '24px', marginBottom: '24px' }}>
|
|
15
|
+
<section style={{ flex: 1 }}>
|
|
16
|
+
<StatCard
|
|
17
|
+
title={t("Total Jobs")}
|
|
18
|
+
value={formatDayJobsValue(dayJobs)}
|
|
19
|
+
icon="MineOperators"
|
|
20
|
+
change={dayJobsChange}
|
|
21
|
+
/>
|
|
22
|
+
</section>
|
|
23
|
+
<section style={{ flex: 1 }}>
|
|
24
|
+
<StatCard
|
|
25
|
+
title={t("Employed Women")}
|
|
26
|
+
value={formatEmployedWomenValue(employedWomen)}
|
|
27
|
+
icon="GenderFemale"
|
|
28
|
+
change={employedWomenChange}
|
|
29
|
+
/>
|
|
30
|
+
</section>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default CommunityStats;
|
|
36
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const GENDER_COLORS = ['#a0ebec', '#00aeb1'];
|
|
2
|
+
|
|
3
|
+
export const getGenderDistributionData = (genderDistribution) => {
|
|
4
|
+
if (Array.isArray(genderDistribution) && genderDistribution.length > 0) {
|
|
5
|
+
const firstItem = genderDistribution[0];
|
|
6
|
+
|
|
7
|
+
if (firstItem && (firstItem.totalFemale !== undefined || firstItem.totalMale !== undefined)) {
|
|
8
|
+
const totalFemale = genderDistribution.reduce((sum, item) => sum + (Number(item.totalFemale) || 0), 0);
|
|
9
|
+
const totalMale = genderDistribution.reduce((sum, item) => sum + (Number(item.totalMale) || 0), 0);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
Male: totalMale,
|
|
13
|
+
Female: totalFemale,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const distribution = {};
|
|
18
|
+
genderDistribution.forEach(item => {
|
|
19
|
+
const gender = item?.gender || item?.label || item?.name || 'Unknown';
|
|
20
|
+
const count = item?.count || item?.value || item?.total || 0;
|
|
21
|
+
const normalizedGender = gender.toLowerCase() === 'male' ? 'Male' :
|
|
22
|
+
gender.toLowerCase() === 'female' ? 'Female' : gender;
|
|
23
|
+
distribution[normalizedGender] = (distribution[normalizedGender] || 0) + count;
|
|
24
|
+
});
|
|
25
|
+
return distribution;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (genderDistribution && typeof genderDistribution === 'object' && !Array.isArray(genderDistribution)) {
|
|
29
|
+
if (genderDistribution.genderDistribution && typeof genderDistribution.genderDistribution === 'object') {
|
|
30
|
+
return genderDistribution.genderDistribution;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (genderDistribution.totalFemale !== undefined || genderDistribution.totalMale !== undefined) {
|
|
34
|
+
return {
|
|
35
|
+
Male: Number(genderDistribution.totalMale) || 0,
|
|
36
|
+
Female: Number(genderDistribution.totalFemale) || 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (genderDistribution.Male !== undefined || genderDistribution.Female !== undefined ||
|
|
41
|
+
genderDistribution.male !== undefined || genderDistribution.female !== undefined) {
|
|
42
|
+
return {
|
|
43
|
+
Male: genderDistribution.Male || genderDistribution.male || 0,
|
|
44
|
+
Female: genderDistribution.Female || genderDistribution.female || 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return genderDistribution;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if the gender distribution data is empty
|
|
56
|
+
* @param {Object} genderDistributionData - Distribution object: { "Male": 10, "Female": 5 }
|
|
57
|
+
* @returns {boolean} True if all values are 0 or empty
|
|
58
|
+
*/
|
|
59
|
+
export const isGenderDistributionEmpty = (genderDistributionData) => {
|
|
60
|
+
return Object.values(genderDistributionData).every(val => !val || val === 0);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Calculates pie chart data from gender distribution
|
|
65
|
+
* @param {Object} genderDistributionData - Distribution object: { "Male": 10, "Female": 5 }
|
|
66
|
+
* @returns {Array} Array of pie chart data points with value, percent, color, label, and key
|
|
67
|
+
*/
|
|
68
|
+
export const calculateGenderPieData = (genderDistributionData) => {
|
|
69
|
+
const total = Object.values(genderDistributionData).reduce((all, val) => all + (val || 0), 0);
|
|
70
|
+
|
|
71
|
+
return Object.keys(genderDistributionData).map((key, index) => {
|
|
72
|
+
const color = GENDER_COLORS[index % GENDER_COLORS.length];
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
value: genderDistributionData[key] || 0,
|
|
76
|
+
percent: total > 0 ? (genderDistributionData[key] || 0) / total : 0,
|
|
77
|
+
color: color,
|
|
78
|
+
label: key,
|
|
79
|
+
key: key,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const getGenderTooltipChildren = (item, isEmpty, genderDistributionData, t, renderTooltipJsx) => {
|
|
85
|
+
if (isEmpty) {
|
|
86
|
+
if (!Object.keys(genderDistributionData).length) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return renderTooltipJsx({
|
|
91
|
+
title: t("Gender Distribution"),
|
|
92
|
+
items: Object.keys(genderDistributionData).map((k) => ({
|
|
93
|
+
color: GENDER_COLORS[Object.keys(genderDistributionData).indexOf(k) % GENDER_COLORS.length],
|
|
94
|
+
label: k,
|
|
95
|
+
value: '0%',
|
|
96
|
+
})),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Calculate total for percentage calculation
|
|
101
|
+
const total = Object.values(genderDistributionData).reduce((all, val) => all + (val || 0), 0);
|
|
102
|
+
|
|
103
|
+
// Show all gender items with percentages (matching the image format)
|
|
104
|
+
return renderTooltipJsx({
|
|
105
|
+
title: t("Gender Distribution"),
|
|
106
|
+
items: Object.keys(genderDistributionData).map((k, index) => {
|
|
107
|
+
const value = genderDistributionData[k] || 0;
|
|
108
|
+
const percent = total > 0 ? Math.round((value / total) * 100) : 0;
|
|
109
|
+
return {
|
|
110
|
+
color: GENDER_COLORS[index % GENDER_COLORS.length],
|
|
111
|
+
label: k,
|
|
112
|
+
value: `${percent}%`,
|
|
113
|
+
};
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useMemo, useCallback } from 'react';
|
|
2
|
+
import { Widget, PieChart } from '../../../../../../../../index.js';
|
|
3
|
+
import { getGenderDistributionData, isGenderDistributionEmpty, calculateGenderPieData, getGenderTooltipChildren } from './helper';
|
|
4
|
+
import { renderTooltipJsx } from '../../../../../../../../utils.js';
|
|
5
|
+
|
|
6
|
+
const GenderDistribution = ({
|
|
7
|
+
genderDistribution,
|
|
8
|
+
loading = false,
|
|
9
|
+
t = (s) => s
|
|
10
|
+
}) => {
|
|
11
|
+
const genderDistributionData = useMemo(() => getGenderDistributionData(genderDistribution), [genderDistribution]);
|
|
12
|
+
const isEmpty = useMemo(() => isGenderDistributionEmpty(genderDistributionData), [genderDistributionData]);
|
|
13
|
+
const pieData = useMemo(() => calculateGenderPieData(genderDistributionData), [genderDistributionData]);
|
|
14
|
+
|
|
15
|
+
const getTooltipChildren = useCallback(
|
|
16
|
+
(item) => getGenderTooltipChildren(item, isEmpty, genderDistributionData, t, renderTooltipJsx),
|
|
17
|
+
[t, isEmpty, genderDistributionData],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Widget
|
|
22
|
+
loading={loading}
|
|
23
|
+
title={<div>{t("Gender Distribution")}</div>}
|
|
24
|
+
className="with-border-header h-w-btn-header "
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
style={{
|
|
28
|
+
marginTop: "auto",
|
|
29
|
+
marginBottom: "auto",
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<PieChart
|
|
33
|
+
mouseXOffset={10}
|
|
34
|
+
mouseYOffset={10}
|
|
35
|
+
changeOpacityOnHover={false}
|
|
36
|
+
data={pieData}
|
|
37
|
+
doConstraints={false}
|
|
38
|
+
isPie
|
|
39
|
+
t={t}
|
|
40
|
+
isEmpty={isEmpty}
|
|
41
|
+
getTooltipChildren={getTooltipChildren}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
</Widget>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default GenderDistribution;
|
|
49
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { Widget, ColumnChart } from '../../../../../../../../index.js';
|
|
4
|
+
|
|
5
|
+
const JobsTimeline = ({
|
|
6
|
+
dayJobsTimeline,
|
|
7
|
+
loading = false,
|
|
8
|
+
t = (s) => s
|
|
9
|
+
}) => {
|
|
10
|
+
|
|
11
|
+
console.log('dayJobsTimeline', dayJobsTimeline);
|
|
12
|
+
const jobsData = Array.isArray(dayJobsTimeline)
|
|
13
|
+
? dayJobsTimeline
|
|
14
|
+
: (dayJobsTimeline?.jobsTimeline || dayJobsTimeline?.jobs || dayJobsTimeline?.timeline || []);
|
|
15
|
+
|
|
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
|
+
const jobsTimelineData = useMemo(() => {
|
|
40
|
+
// Always show last 12 months, even if no data
|
|
41
|
+
const now = dayjs().startOf('month');
|
|
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
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Determine date range
|
|
77
|
+
let minDate = twelveMonthsAgo;
|
|
78
|
+
let maxDate = now;
|
|
79
|
+
|
|
80
|
+
// If we have data, adjust range to include it
|
|
81
|
+
if (dates.length > 0) {
|
|
82
|
+
const sortedDates = dates.sort((a, b) => a.valueOf() - b.valueOf());
|
|
83
|
+
const firstDataDate = sortedDates[0].startOf('month');
|
|
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]);
|
|
117
|
+
|
|
118
|
+
const maxYValue = useMemo(() => {
|
|
119
|
+
if (!jobsTimelineData || jobsTimelineData.length === 0) {
|
|
120
|
+
return 100;
|
|
121
|
+
}
|
|
122
|
+
const maxValue = Math.max(...jobsTimelineData.map(item => item.jobs || 0));
|
|
123
|
+
// If max is 0, set default to 100 to show Y-axis
|
|
124
|
+
// Otherwise, round up to nearest 10
|
|
125
|
+
if (maxValue === 0) {
|
|
126
|
+
return 100;
|
|
127
|
+
}
|
|
128
|
+
return Math.ceil(maxValue / 10) * 10 || 100;
|
|
129
|
+
}, [jobsTimelineData]);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Widget
|
|
133
|
+
title={<div>{t("Day Jobs Timeline")}</div>}
|
|
134
|
+
className="with-border-header h-w-btn-header "
|
|
135
|
+
>
|
|
136
|
+
<ColumnChart
|
|
137
|
+
data={jobsTimelineData}
|
|
138
|
+
xFieldKey="month"
|
|
139
|
+
yFieldKey="jobs"
|
|
140
|
+
animated={true}
|
|
141
|
+
// height={200}
|
|
142
|
+
color="#016C6E"
|
|
143
|
+
renderTooltipContent={(title, data) => {
|
|
144
|
+
if (!data || data.length === 0) return {};
|
|
145
|
+
// For ColumnChart, data structure: data[0]?.data contains the actual data point
|
|
146
|
+
const item = data[0]?.data || data[0];
|
|
147
|
+
const count = item?.jobs || item?.value || 0;
|
|
148
|
+
// Title is the X-axis value (month/date), use it for formatting
|
|
149
|
+
const dateValue = item?.date || title || '';
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
title: t("straatos::local-jobs-created"),
|
|
153
|
+
subTitle: formatDateAxis(dateValue),
|
|
154
|
+
items: [
|
|
155
|
+
{
|
|
156
|
+
label: t("straatos::jobs"),
|
|
157
|
+
value: count,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}}
|
|
162
|
+
formattedXAxis={formatDateAxis}
|
|
163
|
+
formattedYAxis={(value) => {
|
|
164
|
+
return `${value}`.replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => `${s},`);
|
|
165
|
+
}}
|
|
166
|
+
yAxis={{
|
|
167
|
+
min: 0,
|
|
168
|
+
max: maxYValue,
|
|
169
|
+
tickMethod: () => {
|
|
170
|
+
|
|
171
|
+
const step = maxYValue <= 100 ? 5 : Math.max(5, Math.floor(maxYValue / 10));
|
|
172
|
+
const ticks = [];
|
|
173
|
+
for (let i = 0; i <= maxYValue; i += step) {
|
|
174
|
+
ticks.push(i);
|
|
175
|
+
}
|
|
176
|
+
// Ensure max value is included
|
|
177
|
+
if (ticks.length === 0 || ticks[ticks.length - 1] < maxYValue) {
|
|
178
|
+
ticks.push(maxYValue);
|
|
179
|
+
}
|
|
180
|
+
return ticks;
|
|
181
|
+
},
|
|
182
|
+
label: {
|
|
183
|
+
style: {
|
|
184
|
+
fontSize: 12,
|
|
185
|
+
fill: '#666',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
grid: {
|
|
189
|
+
line: {
|
|
190
|
+
style: {
|
|
191
|
+
stroke: '#E5E7EB',
|
|
192
|
+
lineWidth: 1,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}}
|
|
197
|
+
xAxis={{
|
|
198
|
+
label: {
|
|
199
|
+
formatter: formatDateAxis,
|
|
200
|
+
autoHide: true,
|
|
201
|
+
style: {
|
|
202
|
+
fontSize: 12,
|
|
203
|
+
fill: '#666',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
</Widget>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export default JobsTimeline;
|
package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/index.jsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { useWidgetFetch } from '../../../../../../hooks/useWidgetFetch.js';
|
|
3
|
+
import { Widget } from '../../../../../../../../src/index.js';
|
|
4
|
+
import GenderDistribution from './GenderDistribution/index.jsx';
|
|
5
|
+
import JobsTimeline from './JobsTimeline/index.jsx';
|
|
6
|
+
import CommunityStats from './CommunityStats/index.jsx';
|
|
7
|
+
|
|
8
|
+
const CommunityParticipation = ({
|
|
9
|
+
id,
|
|
10
|
+
getSummaryDetail,
|
|
11
|
+
loading = false,
|
|
12
|
+
t = (s) => s
|
|
13
|
+
}) => {
|
|
14
|
+
|
|
15
|
+
const defaultConfig = useMemo(
|
|
16
|
+
() => ({
|
|
17
|
+
basepath: "planting-cycle",
|
|
18
|
+
url: `/summary/${id}/community-participation`,
|
|
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: communityParticipationLoading, data: communityParticipationData } = useWidgetFetch({
|
|
39
|
+
config: defaultConfig,
|
|
40
|
+
getData: customGetData
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const { dayJobs, employedWomen, dayJobsTimeline, genderDistribution } = communityParticipationData;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section>
|
|
47
|
+
<Widget
|
|
48
|
+
title={t("Community Participation")}
|
|
49
|
+
loading={loading}
|
|
50
|
+
className="with-border-header h-w-btn-header"
|
|
51
|
+
>
|
|
52
|
+
<CommunityStats
|
|
53
|
+
dayJobs={dayJobs}
|
|
54
|
+
employedWomen={employedWomen}
|
|
55
|
+
t={t}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<div style={{ display: 'flex', gap: '24px' , }}>
|
|
59
|
+
<section style={{ flex: 1 , height: '100%' }}>
|
|
60
|
+
<JobsTimeline dayJobsTimeline={dayJobsTimeline} loading={communityParticipationLoading} t={t} />
|
|
61
|
+
</section>
|
|
62
|
+
<section style={{ flex: 1 , height: '100%' }}>
|
|
63
|
+
<GenderDistribution genderDistribution={genderDistribution} loading={communityParticipationLoading} t={t} style={{ flex: 1 }} />
|
|
64
|
+
</section>
|
|
65
|
+
</div>
|
|
66
|
+
</Widget>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default CommunityParticipation;
|
|
72
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const PARTNER_COLORS = ['#016C6E', '#00AEB1', '#FF7A45', '#52C41A', '#1890FF', '#722ED1', '#FA8C16', '#EB2F96'];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a direct array of partners to distribution format
|
|
5
|
+
* @param {Array} partnersArray - Array of partner objects: [{ name: "Partner 1", count: 5 }, ...]
|
|
6
|
+
* @returns {Object} Distribution object: { "Partner 1": 5, "Partner 2": 3, ... }
|
|
7
|
+
*/
|
|
8
|
+
export const getCyclePartners = (partnersArray) => {
|
|
9
|
+
if (!Array.isArray(partnersArray)) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const distribution = {};
|
|
14
|
+
partnersArray.forEach(partner => {
|
|
15
|
+
const name = partner?.name || partner?.nickName || partner?.label || 'Unknown';
|
|
16
|
+
const count = partner?.count || partner?.value || 1;
|
|
17
|
+
distribution[name] = (distribution[name] || 0) + count;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return distribution;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks if the partners distribution data is empty
|
|
25
|
+
* @param {Object} partnersDistributionData - Distribution object: { "Partner 1": 5, ... }
|
|
26
|
+
* @returns {boolean} True if all values are 0 or empty
|
|
27
|
+
*/
|
|
28
|
+
export const isPartnersDistributionEmpty = (partnersDistributionData) => {
|
|
29
|
+
return Object.values(partnersDistributionData).every(val => !val || val === 0);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Calculates pie chart data from partners distribution
|
|
34
|
+
* @param {Object} partnersDistributionData - Distribution object: { "Partner 1": 5, ... }
|
|
35
|
+
* @returns {Array} Array of pie chart data points with value, percent, color, label, and key
|
|
36
|
+
*/
|
|
37
|
+
export const calculatePartnersPieData = (partnersDistributionData) => {
|
|
38
|
+
const total = Object.values(partnersDistributionData).reduce((all, val) => all + (val || 0), 0);
|
|
39
|
+
|
|
40
|
+
return Object.keys(partnersDistributionData).map((key, index) => {
|
|
41
|
+
const color = PARTNER_COLORS[index % PARTNER_COLORS.length];
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
value: partnersDistributionData[key] || 0,
|
|
45
|
+
percent: total > 0 ? (partnersDistributionData[key] || 0) / total : 0,
|
|
46
|
+
color: color,
|
|
47
|
+
label: key,
|
|
48
|
+
key: key,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates tooltip content for partners pie chart
|
|
55
|
+
* @param {Object} item - The pie chart item being hovered
|
|
56
|
+
* @param {boolean} isEmpty - Whether the distribution is empty
|
|
57
|
+
* @param {Object} partnersDistributionData - Distribution object
|
|
58
|
+
* @param {Function} t - Translation function
|
|
59
|
+
* @param {Function} renderTooltipJsx - Function to render tooltip JSX
|
|
60
|
+
* @returns {JSX.Element|null} Tooltip content or null
|
|
61
|
+
*/
|
|
62
|
+
export const getPartnersTooltipChildren = (item, isEmpty, partnersDistributionData, t, renderTooltipJsx) => {
|
|
63
|
+
if (isEmpty) {
|
|
64
|
+
if (!Object.keys(partnersDistributionData).length) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return renderTooltipJsx({
|
|
69
|
+
title: t("Cycle Partners"),
|
|
70
|
+
items: Object.keys(partnersDistributionData).map((k) => ({
|
|
71
|
+
label: t("Partner"),
|
|
72
|
+
value: k,
|
|
73
|
+
})),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return renderTooltipJsx({
|
|
78
|
+
title: t("Cycle Partners"),
|
|
79
|
+
items: [
|
|
80
|
+
{
|
|
81
|
+
label: t("Partner"),
|
|
82
|
+
value: item.label || item.key || 'Unknown',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: t("Activities"),
|
|
86
|
+
value: `${item.value || 0}`,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|