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.
Files changed (40) hide show
  1. package/dist/components/index.js +792 -580
  2. package/dist/layouts/index.js +495 -459
  3. package/dist/pages/index.js +2808 -738
  4. package/dist/style/datastake/mapbox-gl.css +330 -0
  5. package/dist/utils/index.js +481 -457
  6. package/package.json +1 -1
  7. package/src/@daf/core/components/Charts/ColumnChart/index.jsx +10 -0
  8. package/src/@daf/core/components/Charts/LineChart/index.jsx +14 -0
  9. package/src/@daf/core/components/Dashboard/Widget/ActivityIndicators/index.jsx +2 -0
  10. package/src/@daf/core/components/Dashboard/Widget/StatCard/StatCard.stories.js +226 -0
  11. package/src/@daf/core/components/Dashboard/Widget/StatCard/index.js +103 -0
  12. package/src/@daf/core/components/Dashboard/Widget/StatCard/style.js +83 -0
  13. package/src/@daf/core/components/Icon/configs/Down.js +8 -0
  14. package/src/@daf/core/components/Icon/configs/Up.js +8 -0
  15. package/src/@daf/core/components/Icon/configs/index.js +4 -0
  16. package/src/@daf/core/components/Icon/configs/partnerIcon.js +1 -1
  17. package/src/@daf/core/components/Sidenav/Menu.jsx +4 -4
  18. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx +43 -0
  19. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/CommunityStats/helper.js +60 -0
  20. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/CommunityStats/index.jsx +36 -0
  21. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/GenderDistribution/helper.js +117 -0
  22. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/GenderDistribution/index.jsx +49 -0
  23. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/JobsTimeline/index.jsx +212 -0
  24. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/index.jsx +72 -0
  25. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/CyclePartners/helper.js +91 -0
  26. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/CyclePartners/index.jsx +50 -0
  27. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/helper.js +134 -0
  28. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/index.jsx +49 -0
  29. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/index.jsx +112 -0
  30. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/index.jsx +498 -0
  31. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/KeyInformation/index.jsx +49 -0
  32. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/PlantingLocations/index.jsx +120 -0
  33. package/src/@daf/pages/Summary/Activities/PlantingCycle/config.js +5 -10
  34. package/src/@daf/pages/Summary/Activities/PlantingCycle/helper.js +218 -0
  35. package/src/@daf/pages/Summary/Activities/PlantingCycle/index.jsx +22 -32
  36. package/src/@daf/pages/Summary/Activities/Restoration/components/ActivityImagery/index.jsx +29 -0
  37. package/src/@daf/pages/Summary/Activities/Restoration/components/ActivityLocation/index.jsx +94 -0
  38. package/src/@daf/pages/Summary/Activities/Restoration/components/WorkersDistribution/index.jsx +49 -0
  39. package/src/@daf/pages/Summary/Activities/Restoration/index.jsx +16 -138
  40. 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;
@@ -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
+