datastake-daf 0.6.773 → 0.6.775

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/components/index.js +2656 -2476
  2. package/dist/hooks/index.js +72 -0
  3. package/dist/pages/index.js +1211 -949
  4. package/dist/utils/index.js +13 -0
  5. package/package.json +1 -1
  6. package/src/@daf/core/components/Dashboard/Map/ChainIcon/Markers/StakeholderMarker.js +8 -76
  7. package/src/@daf/core/components/Dashboard/Map/ChainIcon/index.js +116 -8
  8. package/src/@daf/core/components/Dashboard/Map/ChainIcon/utils.js +73 -17
  9. package/src/@daf/core/components/Dashboard/Map/helper.js +1 -0
  10. package/src/@daf/core/components/Dashboard/Map/hook.js +53 -29
  11. package/src/@daf/core/components/Dashboard/Map/style.js +20 -5
  12. package/src/@daf/hooks/useTimeFilter.js +56 -0
  13. package/src/@daf/hooks/useViewFormUrlParams.js +84 -0
  14. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/config.js +7 -13
  15. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/AssociatedInformation/index.jsx +3 -1
  16. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CommunityParticipation/JobsTimeline/index.jsx +33 -101
  17. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/helper.js +8 -6
  18. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/HealthAndSafety/index.jsx +73 -4
  19. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleIndicators/index.jsx +1 -1
  20. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/PlantingActivitiesTimeline.jsx +148 -0
  21. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/RestoredArea.jsx +150 -0
  22. package/src/@daf/pages/Summary/Activities/PlantingCycle/components/CycleOutcomes/index.jsx +11 -390
  23. package/src/@daf/pages/Summary/Activities/PlantingCycle/index.jsx +2 -2
  24. package/src/@daf/utils/object.js +3 -1
  25. package/src/@daf/utils/timeFilterUtils.js +226 -0
  26. package/src/hooks.js +2 -1
  27. package/src/utils.js +1 -1
@@ -0,0 +1,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
+ }
@@ -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}>{value?.toUpperCase().charAt(0) + value?.slice(1) || '-'}</Tooltip>;
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
- let country = '-';
375
- if (value) {
376
- country = value;
377
- } else if (all.country?.name) {
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
  {
@@ -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
- }), [t, activeTab, projectId, navigate]);
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
- // Always show last 12 months, even if no data
40
- const now = dayjs().startOf('month');
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
- // Determine date range
76
- let minDate = twelveMonthsAgo;
77
- let maxDate = now;
78
-
79
- // If we have data, adjust range to include it
80
- if (dates.length > 0) {
81
- const sortedDates = dates.sort((a, b) => a.valueOf() - b.valueOf());
82
- const firstDataDate = sortedDates[0].startOf('month');
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="month"
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: '#52C41A',
3
- notCompliant: '#FF4D4F',
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("Compliant"),
65
- notCompliant: t("Not Compliant"),
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("Compliant"),
107
- notCompliant: t("Not Compliant"),
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
- activityData,
8
+ id,
9
+ getSummaryDetail,
8
10
  loading = false,
9
11
  t = (s) => s
10
12
  }) => {
11
- const healthAndSafetyDistributionData = useMemo(() => getHealthAndSafetyDistributionData(activityData), [activityData]);
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
-
@@ -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 activityData={indicatorsData} loading={indicatorsLoading} t={t} />
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
+