datastake-daf 0.6.837 → 0.6.839

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.
Binary file
Binary file
Binary file
@@ -0,0 +1,25 @@
1
+ {
2
+ "short_name": "React App",
3
+ "name": "Create React App Sample",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ },
10
+ {
11
+ "src": "logo192.png",
12
+ "type": "image/png",
13
+ "sizes": "192x192"
14
+ },
15
+ {
16
+ "src": "logo512.png",
17
+ "type": "image/png",
18
+ "sizes": "512x512"
19
+ }
20
+ ],
21
+ "start_url": ".",
22
+ "display": "standalone",
23
+ "theme_color": "#000000",
24
+ "background_color": "#ffffff"
25
+ }
@@ -0,0 +1,3 @@
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
@@ -2307,6 +2307,8 @@ const useWidgetFetch = ({
2307
2307
  } = config;
2308
2308
  const [data, setData] = React.useState(defaultData || {});
2309
2309
  const [loading, setLoading] = React.useState(false);
2310
+ const [initFetchDone, setInitFetchDone] = React.useState(false);
2311
+ const isMounted = React.useRef(true);
2310
2312
  const fetchData = async () => {
2311
2313
  if (stop) {
2312
2314
  return;
@@ -2316,6 +2318,7 @@ const useWidgetFetch = ({
2316
2318
  const {
2317
2319
  data
2318
2320
  } = await getData(rest);
2321
+ if (!isMounted.current) return;
2319
2322
  setData(data || defaultData);
2320
2323
  if (isErrorResponse(data)) {
2321
2324
  const errorMessage = getErrorMessage(data);
@@ -2323,18 +2326,28 @@ const useWidgetFetch = ({
2323
2326
  return;
2324
2327
  }
2325
2328
  onFetch();
2329
+ setInitFetchDone(true);
2326
2330
  } catch (err) {
2327
2331
  console.log(err);
2328
2332
  }
2329
- setLoading(false);
2333
+ if (isMounted.current) {
2334
+ setLoading(false);
2335
+ }
2330
2336
  };
2337
+ React.useEffect(() => {
2338
+ isMounted.current = true;
2339
+ return () => {
2340
+ isMounted.current = false;
2341
+ };
2342
+ }, []);
2331
2343
  React.useEffect(() => {
2332
2344
  fetchData();
2333
- }, [JSON.stringify(rest?.filters)]);
2345
+ }, [JSON.stringify(config)]);
2334
2346
  return {
2335
2347
  data,
2336
2348
  loading,
2337
- setData
2349
+ setData,
2350
+ initFetchDone
2338
2351
  };
2339
2352
  };
2340
2353
 
@@ -7506,6 +7506,8 @@ const useWidgetFetch = ({
7506
7506
  } = config;
7507
7507
  const [data, setData] = React.useState(defaultData || {});
7508
7508
  const [loading, setLoading] = React.useState(false);
7509
+ const [initFetchDone, setInitFetchDone] = React.useState(false);
7510
+ const isMounted = React.useRef(true);
7509
7511
  const fetchData = async () => {
7510
7512
  if (stop) {
7511
7513
  return;
@@ -7515,6 +7517,7 @@ const useWidgetFetch = ({
7515
7517
  const {
7516
7518
  data
7517
7519
  } = await getData(rest);
7520
+ if (!isMounted.current) return;
7518
7521
  setData(data || defaultData);
7519
7522
  if (isErrorResponse(data)) {
7520
7523
  const errorMessage = getErrorMessage(data);
@@ -7522,18 +7525,28 @@ const useWidgetFetch = ({
7522
7525
  return;
7523
7526
  }
7524
7527
  onFetch();
7528
+ setInitFetchDone(true);
7525
7529
  } catch (err) {
7526
7530
  console.log(err);
7527
7531
  }
7528
- setLoading(false);
7532
+ if (isMounted.current) {
7533
+ setLoading(false);
7534
+ }
7529
7535
  };
7536
+ React.useEffect(() => {
7537
+ isMounted.current = true;
7538
+ return () => {
7539
+ isMounted.current = false;
7540
+ };
7541
+ }, []);
7530
7542
  React.useEffect(() => {
7531
7543
  fetchData();
7532
- }, [JSON.stringify(rest?.filters)]);
7544
+ }, [JSON.stringify(config)]);
7533
7545
  return {
7534
7546
  data,
7535
7547
  loading,
7536
- setData
7548
+ setData,
7549
+ initFetchDone
7537
7550
  };
7538
7551
  };
7539
7552
 
@@ -55561,7 +55574,7 @@ const PlantingLocations = ({
55561
55574
  return eventsWithGPS.map((event, index) => {
55562
55575
  const locationCheckArrival = event.locationCheckArrival;
55563
55576
  const matchingLocation = locations.find(location => locationCheckArrival.name === location.name || locationCheckArrival._id === location.id || location.id === locationCheckArrival._id) || locations[0];
55564
- const area = matchingLocation?.perimeter ? matchingLocation.perimeter.map(coord => Array.isArray(coord) && coord.length >= 2 ? [coord[1], coord[0]] : coord) : null;
55577
+ const area = matchingLocation?.perimeter ? matchingLocation.perimeter.map(coord => Array.isArray(coord) && coord.length >= 2 ? [coord[0], coord[1]] : coord) : null;
55565
55578
 
55566
55579
  // Only include area if it has at least 3 valid coordinates
55567
55580
  const validArea = area && Array.isArray(area) && area.length >= 3 ? area : null;
@@ -55680,7 +55693,7 @@ const PlantingLocations = ({
55680
55693
  * Formats a date based on the time filter
55681
55694
  * @param {dayjs.Dayjs} date - The date to format
55682
55695
  * @param {boolean} breakLine - Whether to add a line break (for tooltips)
55683
- * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly')
55696
+ * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly', 'yearly')
55684
55697
  * @returns {string} Formatted date string
55685
55698
  */
55686
55699
  const getFormatDate = (date, breakLine = false, timeFilter = 'monthly') => {
@@ -55689,20 +55702,30 @@ const getFormatDate = (date, breakLine = false, timeFilter = 'monthly') => {
55689
55702
  return date.format("DD/MM");
55690
55703
  case "weekly":
55691
55704
  return `W${renderNumber$1(date.week())}`;
55705
+ case "yearly":
55706
+ return date.format("YYYY");
55692
55707
  default:
55693
55708
  // Monthly format: "Dec 24", "Jan 25", etc.
55694
-
55695
55709
  return breakLine ? `${capitalize(date.format("MMM"))}\n${date.format("YY")}` : `${capitalize(date.format("MMM"))} ${date.format("YY")}`;
55696
55710
  }
55697
55711
  };
55698
55712
 
55699
55713
  /**
55700
55714
  * Gets the time quantity string for dayjs operations
55701
- * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly')
55702
- * @returns {string} Time quantity string ('days', 'weeks', 'months')
55715
+ * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly', 'yearly')
55716
+ * @returns {string} Time quantity string ('days', 'weeks', 'months', 'years')
55703
55717
  */
55704
55718
  const getTimeQuantity = (timeFilter = 'monthly') => {
55705
- return timeFilter === "monthly" ? "months" : timeFilter === "daily" ? "days" : "weeks";
55719
+ switch (timeFilter) {
55720
+ case "daily":
55721
+ return "days";
55722
+ case "weekly":
55723
+ return "weeks";
55724
+ case "yearly":
55725
+ return "years";
55726
+ default:
55727
+ return "months";
55728
+ }
55706
55729
  };
55707
55730
 
55708
55731
  /**
@@ -55728,6 +55751,9 @@ const getPreviousGraphData = (dates, startDate, timeFilter, valueField = 'total'
55728
55751
  case "weekly":
55729
55752
  isBeforeStart = date.isBefore(startDate, 'week');
55730
55753
  break;
55754
+ case "yearly":
55755
+ isBeforeStart = date.isBefore(startDate, 'year');
55756
+ break;
55731
55757
  default:
55732
55758
  isBeforeStart = date.isBefore(startDate, 'month');
55733
55759
  break;
@@ -55750,7 +55776,7 @@ const getPreviousGraphData = (dates, startDate, timeFilter, valueField = 'total'
55750
55776
  * Processes chart data with time filtering support
55751
55777
  * @param {Object} params - Parameters object
55752
55778
  * @param {Array} params.mainData - Array of data objects with date and value fields
55753
- * @param {string} params.timeFilter - Time filter ('daily', 'weekly', 'monthly')
55779
+ * @param {string} params.timeFilter - Time filter ('daily', 'weekly', 'monthly', 'yearly')
55754
55780
  * @param {Object} params.filters - Optional filters object with timeframe
55755
55781
  * @param {boolean} params.isCumulative - Whether to calculate cumulative values (default: false)
55756
55782
  * @param {string} params.valueField - Field name to extract value from (default: 'total', also checks 'count', 'jobs', 'value')
@@ -55777,6 +55803,9 @@ const processChartDateData = ({
55777
55803
  } else if (filter === "weekly") {
55778
55804
  start = start.startOf('week');
55779
55805
  end = end.startOf('week');
55806
+ } else if (filter === "yearly") {
55807
+ start = start.startOf('year');
55808
+ end = end.startOf('year');
55780
55809
  } else {
55781
55810
  start = start.startOf('month');
55782
55811
  end = end.startOf('month');
@@ -55793,9 +55822,24 @@ const processChartDateData = ({
55793
55822
  cumulativeScore = hasPreviousData ? previousCumulativeScore : 0;
55794
55823
  }
55795
55824
 
55825
+ // Get the period unit for comparison
55826
+ const getPeriodUnit = f => {
55827
+ switch (f) {
55828
+ case "daily":
55829
+ return "day";
55830
+ case "weekly":
55831
+ return "week";
55832
+ case "yearly":
55833
+ return "year";
55834
+ default:
55835
+ return "month";
55836
+ }
55837
+ };
55838
+ const periodUnit = getPeriodUnit(filter);
55839
+
55796
55840
  // Loop until we reach the end date
55797
55841
  let currentDate = start.clone();
55798
- while (currentDate.isBefore(end) || currentDate.isSame(end, filter === "daily" ? "day" : filter === "weekly" ? "week" : "month")) {
55842
+ while (currentDate.isBefore(end) || currentDate.isSame(end, periodUnit)) {
55799
55843
  // Filter data points that fall within this period
55800
55844
  const score = isEmpty ? 0 : dates.filter(d => {
55801
55845
  if (!d.date) return false;
@@ -55804,6 +55848,8 @@ const processChartDateData = ({
55804
55848
  return d.date === currentDate.format("YYYY-MM-DD");
55805
55849
  case "weekly":
55806
55850
  return dayjs__default["default"](d.date, "YYYY-MM-DD").week() === currentDate.week() && dayjs__default["default"](d.date, "YYYY-MM-DD").year() === currentDate.year();
55851
+ case "yearly":
55852
+ return dayjs__default["default"](d.date, "YYYY-MM-DD").year() === currentDate.year();
55807
55853
  default:
55808
55854
  return dayjs__default["default"](d.date, "YYYY-MM-DD").format("YYYY-MM") === currentDate.format("YYYY-MM");
55809
55855
  }
@@ -55840,7 +55886,7 @@ const processChartDateData = ({
55840
55886
  const formatDateAxis = (label, getFormatDateFn) => {
55841
55887
  if (!label) return label;
55842
55888
 
55843
- // Check if label is already in the correct format (MMM YY, DD/MM, or W#)
55889
+ // Check if label is already in the correct format (MMM YY, DD/MM, W#, or YYYY)
55844
55890
  // If it matches our format patterns, return as-is
55845
55891
  if (typeof label === 'string') {
55846
55892
  // Check for MMM YY format (e.g., "Dec 24", "Jan 25")
@@ -55855,6 +55901,10 @@ const formatDateAxis = (label, getFormatDateFn) => {
55855
55901
  if (/^W\d+$/.test(label)) {
55856
55902
  return label;
55857
55903
  }
55904
+ // Check for YYYY format (e.g., "2024", "2025")
55905
+ if (/^\d{4}$/.test(label)) {
55906
+ return label;
55907
+ }
55858
55908
  }
55859
55909
 
55860
55910
  // Otherwise, try to parse and format it
@@ -55862,7 +55912,7 @@ const formatDateAxis = (label, getFormatDateFn) => {
55862
55912
 
55863
55913
  // If first attempt fails, try parsing as ISO date string
55864
55914
  if (!date.isValid() && typeof label === 'string') {
55865
- date = dayjs__default["default"](label, ['YYYY-MM-DD', 'YYYY-MM', 'MMM YY', 'MMM YYYY', 'DD/MM'], true);
55915
+ date = dayjs__default["default"](label, ['YYYY-MM-DD', 'YYYY-MM', 'YYYY', 'MMM YY', 'MMM YYYY', 'DD/MM'], true);
55866
55916
  }
55867
55917
 
55868
55918
  // If it's a valid date, format it using getFormatDate
@@ -55879,7 +55929,7 @@ const formatDateAxis = (label, getFormatDateFn) => {
55879
55929
  * Provides state management and formatting functions for time-based charts
55880
55930
  *
55881
55931
  * @param {Object} options - Configuration options
55882
- * @param {string} options.defaultFilter - Default time filter ('daily', 'weekly', 'monthly')
55932
+ * @param {string} options.defaultFilter - Default time filter ('daily', 'weekly', 'monthly', 'yearly')
55883
55933
  * @returns {Object} Time filter state and utilities
55884
55934
  */
55885
55935
  const useTimeFilter = ({
@@ -55930,6 +55980,9 @@ const selectOptions$2 = [{
55930
55980
  }, {
55931
55981
  label: "Monthly",
55932
55982
  value: "monthly"
55983
+ }, {
55984
+ label: "Yearly",
55985
+ value: "yearly"
55933
55986
  }];
55934
55987
  const RestoredArea = ({
55935
55988
  restoredAreaChart,
@@ -56073,6 +56126,9 @@ const selectOptions$1 = [{
56073
56126
  }, {
56074
56127
  label: "Monthly",
56075
56128
  value: "monthly"
56129
+ }, {
56130
+ label: "Yearly",
56131
+ value: "yearly"
56076
56132
  }];
56077
56133
  const PlantingActivitiesTimeline = ({
56078
56134
  activitiesTimelineChart,
@@ -56101,6 +56157,11 @@ const PlantingActivitiesTimeline = ({
56101
56157
  while (lastNonZeroIndex >= 0 && (processedData[lastNonZeroIndex].jobs || 0) === 0) {
56102
56158
  lastNonZeroIndex--;
56103
56159
  }
56160
+
56161
+ // If all values are 0, return full processed data to show x-axis with default date range
56162
+ if (lastNonZeroIndex < 0) {
56163
+ return processedData;
56164
+ }
56104
56165
  return processedData.slice(0, lastNonZeroIndex + 1);
56105
56166
  }, [activitiesTimelineChart, processChartDateData]);
56106
56167
  const maxActivitiesYValue = React.useMemo(() => {
@@ -56995,6 +57056,9 @@ const selectOptions = [{
56995
57056
  }, {
56996
57057
  label: "Monthly",
56997
57058
  value: "monthly"
57059
+ }, {
57060
+ label: "Yearly",
57061
+ value: "yearly"
56998
57062
  }];
56999
57063
  const JobsTimeline = ({
57000
57064
  dayJobsTimeline,
@@ -57010,22 +57074,6 @@ const JobsTimeline = ({
57010
57074
  defaultFilter: 'monthly'
57011
57075
  });
57012
57076
  const jobsData = Array.isArray(dayJobsTimeline) ? dayJobsTimeline : dayJobsTimeline?.jobsTimeline || dayJobsTimeline?.jobs || dayJobsTimeline?.timeline || [];
57013
-
57014
- // const jobsTimelineData = useMemo(() => {
57015
- // // Always process data, even if empty, to generate default date range for x-axis
57016
- // const dataToProcess = (!jobsData || !Array.isArray(jobsData) || jobsData.length === 0)
57017
- // ? []
57018
- // : jobsData;
57019
-
57020
- // // Process data without cumulative calculation (for jobs timeline)
57021
- // // Try to find value in total, count, jobs, or value fields
57022
- // return processChartDateData({
57023
- // mainData: dataToProcess,
57024
- // isCumulative: false,
57025
- // valueField: 'total', // Will fallback to count/jobs/value if total doesn't exist
57026
- // });
57027
- // }, [jobsData, processChartDateData]);
57028
-
57029
57077
  const jobsTimelineData = React.useMemo(() => {
57030
57078
  // Prepare data first
57031
57079
  const dataToProcess = !jobsData || !Array.isArray(jobsData) || jobsData.length === 0 ? [] : jobsData;
@@ -57043,7 +57091,12 @@ const JobsTimeline = ({
57043
57091
  lastNonZeroIndex--;
57044
57092
  }
57045
57093
 
57046
- // Slice up to last period with data
57094
+ // If all values are 0, return full processed data to show x-axis with default date range
57095
+ if (lastNonZeroIndex < 0) {
57096
+ return processedData;
57097
+ }
57098
+
57099
+ // Otherwise, slice up to last period with data
57047
57100
  return processedData.slice(0, lastNonZeroIndex + 1);
57048
57101
  }, [jobsData, processChartDateData]);
57049
57102
  const maxYValue = React.useMemo(() => {
@@ -0,0 +1,330 @@
1
+ /* Isolated Mapbox GL CSS - Scoped to prevent Leaflet conflicts */
2
+
3
+ /* Mapbox GL Core Styles - Scoped with .mapbox-gl-scope */
4
+ .mapbox-gl-scope .mapboxgl-map {
5
+ font: 12px/20px Helvetica Neue, Arial, Helvetica, sans-serif;
6
+ overflow: hidden;
7
+ position: relative;
8
+ -webkit-tap-highlight-color: rgb(0 0 0/0);
9
+ }
10
+
11
+ .mapbox-gl-scope .mapboxgl-canvas {
12
+ left: 0;
13
+ position: absolute;
14
+ top: 0;
15
+ }
16
+
17
+ .mapbox-gl-scope .mapboxgl-map:-webkit-full-screen {
18
+ height: 100%;
19
+ width: 100%;
20
+ }
21
+
22
+ .mapbox-gl-scope .mapboxgl-canary {
23
+ background-color: salmon;
24
+ }
25
+
26
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-interactive,
27
+ .mapbox-gl-scope .mapboxgl-ctrl-group button.mapboxgl-ctrl-compass {
28
+ cursor: grab;
29
+ -webkit-user-select: none;
30
+ user-select: none;
31
+ }
32
+
33
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-interactive.mapboxgl-track-pointer {
34
+ cursor: pointer;
35
+ }
36
+
37
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-interactive:active,
38
+ .mapbox-gl-scope .mapboxgl-ctrl-group button.mapboxgl-ctrl-compass:active {
39
+ cursor: grabbing;
40
+ }
41
+
42
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate,
43
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate .mapboxgl-canvas {
44
+ touch-action: pan-x pan-y;
45
+ }
46
+
47
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-drag-pan,
48
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-drag-pan .mapboxgl-canvas {
49
+ touch-action: pinch-zoom;
50
+ }
51
+
52
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan,
53
+ .mapbox-gl-scope .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan .mapboxgl-canvas {
54
+ touch-action: none;
55
+ }
56
+
57
+ /* Control positioning */
58
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom,
59
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-left,
60
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-right,
61
+ .mapbox-gl-scope .mapboxgl-ctrl-left,
62
+ .mapbox-gl-scope .mapboxgl-ctrl-right,
63
+ .mapbox-gl-scope .mapboxgl-ctrl-top,
64
+ .mapbox-gl-scope .mapboxgl-ctrl-top-left,
65
+ .mapbox-gl-scope .mapboxgl-ctrl-top-right {
66
+ pointer-events: none;
67
+ position: absolute;
68
+ z-index: 2;
69
+ }
70
+
71
+ .mapbox-gl-scope .mapboxgl-ctrl-top-left {
72
+ left: 0;
73
+ top: 0;
74
+ }
75
+
76
+ .mapbox-gl-scope .mapboxgl-ctrl-top {
77
+ left: 50%;
78
+ top: 0;
79
+ transform: translateX(-50%);
80
+ }
81
+
82
+ .mapbox-gl-scope .mapboxgl-ctrl-top-right {
83
+ right: 0;
84
+ top: 0;
85
+ }
86
+
87
+ .mapbox-gl-scope .mapboxgl-ctrl-right {
88
+ right: 0;
89
+ top: 50%;
90
+ transform: translateY(-50%);
91
+ }
92
+
93
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-right {
94
+ bottom: 0;
95
+ right: 0;
96
+ }
97
+
98
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom {
99
+ bottom: 0;
100
+ left: 50%;
101
+ transform: translateX(-50%);
102
+ }
103
+
104
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-left {
105
+ bottom: 0;
106
+ left: 0;
107
+ }
108
+
109
+ .mapbox-gl-scope .mapboxgl-ctrl-left {
110
+ left: 0;
111
+ top: 50%;
112
+ transform: translateY(-50%);
113
+ }
114
+
115
+ .mapbox-gl-scope .mapboxgl-ctrl {
116
+ clear: both;
117
+ pointer-events: auto;
118
+ transform: translate(0);
119
+ }
120
+
121
+ .mapbox-gl-scope .mapboxgl-ctrl-top-left .mapboxgl-ctrl {
122
+ float: left;
123
+ margin: 10px 0 0 10px;
124
+ }
125
+
126
+ .mapbox-gl-scope .mapboxgl-ctrl-top .mapboxgl-ctrl {
127
+ float: left;
128
+ margin: 10px 0;
129
+ }
130
+
131
+ .mapbox-gl-scope .mapboxgl-ctrl-top-right .mapboxgl-ctrl {
132
+ float: right;
133
+ margin: 10px 10px 0 0;
134
+ }
135
+
136
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-right .mapboxgl-ctrl,
137
+ .mapbox-gl-scope .mapboxgl-ctrl-right .mapboxgl-ctrl {
138
+ float: right;
139
+ margin: 0 10px 10px 0;
140
+ }
141
+
142
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom .mapboxgl-ctrl {
143
+ float: left;
144
+ margin: 10px 0;
145
+ }
146
+
147
+ .mapbox-gl-scope .mapboxgl-ctrl-bottom-left .mapboxgl-ctrl,
148
+ .mapbox-gl-scope .mapboxgl-ctrl-left .mapboxgl-ctrl {
149
+ float: left;
150
+ margin: 0 0 10px 10px;
151
+ }
152
+
153
+ /* Control group styling */
154
+ .mapbox-gl-scope .mapboxgl-ctrl-group {
155
+ background: #fff;
156
+ border-radius: 4px;
157
+ }
158
+
159
+ .mapbox-gl-scope .mapboxgl-ctrl-group:not(:empty) {
160
+ box-shadow: 0 0 0 2px #0000001a;
161
+ }
162
+
163
+ .mapbox-gl-scope .mapboxgl-ctrl-group button {
164
+ background-color: initial;
165
+ border: 0;
166
+ box-sizing: border-box;
167
+ cursor: pointer;
168
+ display: block;
169
+ height: 29px;
170
+ outline: none;
171
+ overflow: hidden;
172
+ padding: 0;
173
+ width: 29px;
174
+ }
175
+
176
+ .mapbox-gl-scope .mapboxgl-ctrl-group button+button {
177
+ border-top: 1px solid #ddd;
178
+ }
179
+
180
+ .mapbox-gl-scope .mapboxgl-ctrl button .mapboxgl-ctrl-icon {
181
+ background-position: 50%;
182
+ background-repeat: no-repeat;
183
+ display: block;
184
+ height: 100%;
185
+ width: 100%;
186
+ }
187
+
188
+ .mapbox-gl-scope .mapboxgl-ctrl-attrib-button:focus,
189
+ .mapbox-gl-scope .mapboxgl-ctrl-group button:focus {
190
+ box-shadow: 0 0 2px 2px #0096ff;
191
+ }
192
+
193
+ .mapbox-gl-scope .mapboxgl-ctrl button:disabled {
194
+ cursor: not-allowed;
195
+ }
196
+
197
+ .mapbox-gl-scope .mapboxgl-ctrl button:disabled .mapboxgl-ctrl-icon {
198
+ opacity: .25;
199
+ }
200
+
201
+ .mapbox-gl-scope .mapboxgl-ctrl-group button:first-child {
202
+ border-radius: 4px 4px 0 0;
203
+ }
204
+
205
+ .mapbox-gl-scope .mapboxgl-ctrl-group button:last-child {
206
+ border-radius: 0 0 4px 4px;
207
+ }
208
+
209
+ .mapbox-gl-scope .mapboxgl-ctrl-group button:only-child {
210
+ border-radius: inherit;
211
+ }
212
+
213
+ .mapbox-gl-scope .mapboxgl-ctrl button:not(:disabled):hover {
214
+ background-color: #0000000d;
215
+ }
216
+
217
+ /* Marker styles */
218
+ .mapbox-gl-scope .mapboxgl-marker {
219
+ position: absolute;
220
+ z-index: 1;
221
+ }
222
+
223
+ .mapbox-gl-scope .mapboxgl-marker svg {
224
+ display: block;
225
+ }
226
+
227
+ /* Popup styles */
228
+ .mapbox-gl-scope .mapboxgl-popup {
229
+ position: absolute;
230
+ text-align: center;
231
+ margin-bottom: 20px;
232
+ }
233
+
234
+ .mapbox-gl-scope .mapboxgl-popup-content-wrapper {
235
+ padding: 1px;
236
+ text-align: left;
237
+ border-radius: 12px;
238
+ }
239
+
240
+ .mapbox-gl-scope .mapboxgl-popup-content {
241
+ margin: 13px 24px 13px 20px;
242
+ line-height: 1.3;
243
+ font-size: 13px;
244
+ min-height: 1px;
245
+ }
246
+
247
+ .mapbox-gl-scope .mapboxgl-popup-content p {
248
+ margin: 17px 0;
249
+ }
250
+
251
+ .mapbox-gl-scope .mapboxgl-popup-tip-container {
252
+ width: 40px;
253
+ height: 20px;
254
+ position: absolute;
255
+ left: 50%;
256
+ margin-top: -1px;
257
+ margin-left: -20px;
258
+ overflow: hidden;
259
+ pointer-events: none;
260
+ }
261
+
262
+ .mapbox-gl-scope .mapboxgl-popup-tip {
263
+ width: 17px;
264
+ height: 17px;
265
+ padding: 1px;
266
+ margin: -10px auto 0;
267
+ pointer-events: auto;
268
+ -webkit-transform: rotate(45deg);
269
+ -moz-transform: rotate(45deg);
270
+ -ms-transform: rotate(45deg);
271
+ transform: rotate(45deg);
272
+ }
273
+
274
+ .mapbox-gl-scope .mapboxgl-popup-content-wrapper,
275
+ .mapbox-gl-scope .mapboxgl-popup-tip {
276
+ background: white;
277
+ color: #333;
278
+ box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
279
+ }
280
+
281
+ .mapbox-gl-scope .mapboxgl-popup-close-button {
282
+ position: absolute;
283
+ top: 0;
284
+ right: 0;
285
+ border: none;
286
+ text-align: center;
287
+ width: 24px;
288
+ height: 24px;
289
+ font: 16px/24px Tahoma, Verdana, sans-serif;
290
+ color: #757575;
291
+ text-decoration: none;
292
+ background: transparent;
293
+ }
294
+
295
+ .mapbox-gl-scope .mapboxgl-popup-close-button:hover,
296
+ .mapbox-gl-scope .mapboxgl-popup-close-button:focus {
297
+ color: #585858;
298
+ }
299
+
300
+ /* Attribution */
301
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution {
302
+ background: #fff;
303
+ background: rgba(255, 255, 255, 0.8);
304
+ margin: 0;
305
+ }
306
+
307
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution,
308
+ .mapbox-gl-scope .mapboxgl-ctrl-scale-line {
309
+ padding: 0 5px;
310
+ color: #333;
311
+ line-height: 1.4;
312
+ }
313
+
314
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution a {
315
+ text-decoration: none;
316
+ }
317
+
318
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution a:hover,
319
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution a:focus {
320
+ text-decoration: underline;
321
+ }
322
+
323
+ /* Hide attribution by default */
324
+ .mapbox-gl-scope .mapboxgl-ctrl-attribution {
325
+ display: none !important;
326
+ }
327
+
328
+ .mapbox-gl-scope .mapboxgl-ctrl-logo {
329
+ display: none !important;
330
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datastake-daf",
3
- "version": "0.6.837",
3
+ "version": "0.6.839",
4
4
  "dependencies": {
5
5
  "@ant-design/icons": "^5.2.5",
6
6
  "@antv/g2": "^5.1.1",
@@ -6,7 +6,7 @@ import { getFormatDate, getTimeQuantity, processChartDateData, formatDateAxis }
6
6
  * Provides state management and formatting functions for time-based charts
7
7
  *
8
8
  * @param {Object} options - Configuration options
9
- * @param {string} options.defaultFilter - Default time filter ('daily', 'weekly', 'monthly')
9
+ * @param {string} options.defaultFilter - Default time filter ('daily', 'weekly', 'monthly', 'yearly')
10
10
  * @returns {Object} Time filter state and utilities
11
11
  */
12
12
  export const useTimeFilter = ({ defaultFilter = 'monthly' } = {}) => {
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from "react";
1
+ import { useState, useEffect, useRef } from "react";
2
2
  import DashboardService from "../services/DashboardService.js";
3
3
  import { isErrorResponse, getErrorMessage } from "../../helpers/errorHandling.js";
4
4
  import { message } from "antd";
@@ -15,7 +15,8 @@ export const useWidgetFetch = ({config, getData = DashboardService.getWidget, on
15
15
  const { stop, defaultData, ...rest} = config;
16
16
  const [ data, setData ] = useState(defaultData || {});
17
17
  const [ loading, setLoading ] = useState(false);
18
-
18
+ const [ initFetchDone, setInitFetchDone ] = useState(false);
19
+ const isMounted = useRef(true);
19
20
 
20
21
  const fetchData = async () => {
21
22
  if (stop) {
@@ -26,27 +27,39 @@ export const useWidgetFetch = ({config, getData = DashboardService.getWidget, on
26
27
 
27
28
  try {
28
29
  const { data } = await getData(rest);
30
+ if (!isMounted.current) return;
29
31
  setData(data || defaultData);
30
32
  if (isErrorResponse(data)) {
31
33
  const errorMessage = getErrorMessage(data);
32
34
  message.error(errorMessage);
33
35
  return;
34
36
  }
35
- onFetch()
37
+ onFetch();
38
+ setInitFetchDone(true);
36
39
  } catch (err) {
37
40
  console.log(err);
38
41
  }
39
42
 
40
- setLoading(false);
43
+ if (isMounted.current) {
44
+ setLoading(false);
45
+ }
41
46
  };
42
47
 
43
48
  useEffect(() => {
44
- fetchData();
45
- }, [JSON.stringify(rest?.filters)]);
49
+ isMounted.current = true;
50
+ return () => {
51
+ isMounted.current = false;
52
+ };
53
+ }, []);
54
+
55
+ useEffect(() => {
56
+ fetchData();
57
+ }, [JSON.stringify(config)]);
46
58
 
47
59
  return {
48
60
  data,
49
61
  loading,
50
- setData
62
+ setData,
63
+ initFetchDone
51
64
  }
52
65
  }
@@ -7,6 +7,7 @@ const selectOptions = [
7
7
  { label: "Daily", value: "daily" },
8
8
  { label: "Weekly", value: "weekly" },
9
9
  { label: "Monthly", value: "monthly" },
10
+ { label: "Yearly", value: "yearly" },
10
11
  ];
11
12
 
12
13
  const JobsTimeline = ({
@@ -20,20 +21,6 @@ const JobsTimeline = ({
20
21
  ? dayJobsTimeline
21
22
  : (dayJobsTimeline?.jobsTimeline || dayJobsTimeline?.jobs || dayJobsTimeline?.timeline || []);
22
23
 
23
- // const jobsTimelineData = useMemo(() => {
24
- // // Always process data, even if empty, to generate default date range for x-axis
25
- // const dataToProcess = (!jobsData || !Array.isArray(jobsData) || jobsData.length === 0)
26
- // ? []
27
- // : jobsData;
28
-
29
- // // Process data without cumulative calculation (for jobs timeline)
30
- // // Try to find value in total, count, jobs, or value fields
31
- // return processChartDateData({
32
- // mainData: dataToProcess,
33
- // isCumulative: false,
34
- // valueField: 'total', // Will fallback to count/jobs/value if total doesn't exist
35
- // });
36
- // }, [jobsData, processChartDateData]);
37
24
 
38
25
  const jobsTimelineData = useMemo(() => {
39
26
  // Prepare data first
@@ -54,7 +41,12 @@ const JobsTimeline = ({
54
41
  lastNonZeroIndex--;
55
42
  }
56
43
 
57
- // Slice up to last period with data
44
+ // If all values are 0, return full processed data to show x-axis with default date range
45
+ if (lastNonZeroIndex < 0) {
46
+ return processedData;
47
+ }
48
+
49
+ // Otherwise, slice up to last period with data
58
50
  return processedData.slice(0, lastNonZeroIndex + 1);
59
51
  }, [jobsData, processChartDateData]);
60
52
 
@@ -7,6 +7,7 @@ const selectOptions = [
7
7
  { label: "Daily", value: "daily" },
8
8
  { label: "Weekly", value: "weekly" },
9
9
  { label: "Monthly", value: "monthly" },
10
+ { label: "Yearly", value: "yearly" },
10
11
  ];
11
12
 
12
13
  const PlantingActivitiesTimeline = ({
@@ -34,6 +35,11 @@ const PlantingActivitiesTimeline = ({
34
35
  lastNonZeroIndex--;
35
36
  }
36
37
 
38
+ // If all values are 0, return full processed data to show x-axis with default date range
39
+ if (lastNonZeroIndex < 0) {
40
+ return processedData;
41
+ }
42
+
37
43
  return processedData.slice(0, lastNonZeroIndex + 1);
38
44
  }, [activitiesTimelineChart, processChartDateData]);
39
45
 
@@ -7,6 +7,7 @@ const selectOptions = [
7
7
  { label: "Daily", value: "daily" },
8
8
  { label: "Weekly", value: "weekly" },
9
9
  { label: "Monthly", value: "monthly" },
10
+ { label: "Yearly", value: "yearly" },
10
11
  ];
11
12
 
12
13
  const RestoredArea = ({
@@ -64,11 +64,11 @@ const PlantingLocations = ({
64
64
  ) || locations[0];
65
65
 
66
66
 
67
- const area = matchingLocation?.perimeter ? matchingLocation.perimeter.map(coord =>
68
- Array.isArray(coord) && coord.length >= 2
69
- ? [coord[1], coord[0]]
70
- : coord
71
- ) : null;
67
+ const area = matchingLocation?.perimeter ? matchingLocation.perimeter.map(coord =>
68
+ Array.isArray(coord) && coord.length >= 2
69
+ ? [coord[0], coord[1]]
70
+ : coord
71
+ ) : null;
72
72
 
73
73
  // Only include area if it has at least 3 valid coordinates
74
74
  const validArea = area && Array.isArray(area) && area.length >= 3 ? area : null;
@@ -6,18 +6,19 @@ import { renderNumber } from './numbers.js';
6
6
  * Formats a date based on the time filter
7
7
  * @param {dayjs.Dayjs} date - The date to format
8
8
  * @param {boolean} breakLine - Whether to add a line break (for tooltips)
9
- * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly')
9
+ * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly', 'yearly')
10
10
  * @returns {string} Formatted date string
11
11
  */
12
12
  export const getFormatDate = (date, breakLine = false, timeFilter = 'monthly') => {
13
13
  switch (timeFilter) {
14
14
  case "daily":
15
15
  return date.format("DD/MM");
16
- case "weekly" :
16
+ case "weekly":
17
17
  return `W${renderNumber(date.week())}`;
18
+ case "yearly":
19
+ return date.format("YYYY");
18
20
  default:
19
21
  // Monthly format: "Dec 24", "Jan 25", etc.
20
-
21
22
  return breakLine
22
23
  ? `${capitalize(date.format("MMM"))}\n${date.format("YY")}`
23
24
  : `${capitalize(date.format("MMM"))} ${date.format("YY")}`;
@@ -26,15 +27,20 @@ export const getFormatDate = (date, breakLine = false, timeFilter = 'monthly') =
26
27
 
27
28
  /**
28
29
  * Gets the time quantity string for dayjs operations
29
- * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly')
30
- * @returns {string} Time quantity string ('days', 'weeks', 'months')
30
+ * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly', 'yearly')
31
+ * @returns {string} Time quantity string ('days', 'weeks', 'months', 'years')
31
32
  */
32
33
  export const getTimeQuantity = (timeFilter = 'monthly') => {
33
- return timeFilter === "monthly"
34
- ? "months"
35
- : timeFilter === "daily"
36
- ? "days"
37
- : "weeks";
34
+ switch (timeFilter) {
35
+ case "daily":
36
+ return "days";
37
+ case "weekly":
38
+ return "weeks";
39
+ case "yearly":
40
+ return "years";
41
+ default:
42
+ return "months";
43
+ }
38
44
  };
39
45
 
40
46
  /**
@@ -62,6 +68,9 @@ export const getPreviousGraphData = (dates, startDate, timeFilter, valueField =
62
68
  case "weekly":
63
69
  isBeforeStart = date.isBefore(startDate, 'week');
64
70
  break;
71
+ case "yearly":
72
+ isBeforeStart = date.isBefore(startDate, 'year');
73
+ break;
65
74
  default:
66
75
  isBeforeStart = date.isBefore(startDate, 'month');
67
76
  break;
@@ -86,7 +95,7 @@ export const getPreviousGraphData = (dates, startDate, timeFilter, valueField =
86
95
  * Processes chart data with time filtering support
87
96
  * @param {Object} params - Parameters object
88
97
  * @param {Array} params.mainData - Array of data objects with date and value fields
89
- * @param {string} params.timeFilter - Time filter ('daily', 'weekly', 'monthly')
98
+ * @param {string} params.timeFilter - Time filter ('daily', 'weekly', 'monthly', 'yearly')
90
99
  * @param {Object} params.filters - Optional filters object with timeframe
91
100
  * @param {boolean} params.isCumulative - Whether to calculate cumulative values (default: false)
92
101
  * @param {string} params.valueField - Field name to extract value from (default: 'total', also checks 'count', 'jobs', 'value')
@@ -114,6 +123,9 @@ export const processChartDateData = ({
114
123
  } else if (filter === "weekly") {
115
124
  start = start.startOf('week');
116
125
  end = end.startOf('week');
126
+ } else if (filter === "yearly") {
127
+ start = start.startOf('year');
128
+ end = end.startOf('year');
117
129
  } else {
118
130
  start = start.startOf('month');
119
131
  end = end.startOf('month');
@@ -133,9 +145,20 @@ export const processChartDateData = ({
133
145
  cumulativeScore = hasPreviousData ? previousCumulativeScore : 0;
134
146
  }
135
147
 
148
+ // Get the period unit for comparison
149
+ const getPeriodUnit = (f) => {
150
+ switch (f) {
151
+ case "daily": return "day";
152
+ case "weekly": return "week";
153
+ case "yearly": return "year";
154
+ default: return "month";
155
+ }
156
+ };
157
+ const periodUnit = getPeriodUnit(filter);
158
+
136
159
  // Loop until we reach the end date
137
160
  let currentDate = start.clone();
138
- while (currentDate.isBefore(end) || currentDate.isSame(end, filter === "daily" ? "day" : filter === "weekly" ? "week" : "month")) {
161
+ while (currentDate.isBefore(end) || currentDate.isSame(end, periodUnit)) {
139
162
  // Filter data points that fall within this period
140
163
  const score = isEmpty ? 0 : dates
141
164
  .filter((d) => {
@@ -146,6 +169,8 @@ export const processChartDateData = ({
146
169
  case "weekly":
147
170
  return dayjs(d.date, "YYYY-MM-DD").week() === currentDate.week() &&
148
171
  dayjs(d.date, "YYYY-MM-DD").year() === currentDate.year();
172
+ case "yearly":
173
+ return dayjs(d.date, "YYYY-MM-DD").year() === currentDate.year();
149
174
  default:
150
175
  return (
151
176
  dayjs(d.date, "YYYY-MM-DD").format("YYYY-MM") ===
@@ -187,7 +212,7 @@ export const processChartDateData = ({
187
212
  export const formatDateAxis = (label, getFormatDateFn) => {
188
213
  if (!label) return label;
189
214
 
190
- // Check if label is already in the correct format (MMM YY, DD/MM, or W#)
215
+ // Check if label is already in the correct format (MMM YY, DD/MM, W#, or YYYY)
191
216
  // If it matches our format patterns, return as-is
192
217
  if (typeof label === 'string') {
193
218
  // Check for MMM YY format (e.g., "Dec 24", "Jan 25")
@@ -202,6 +227,10 @@ export const formatDateAxis = (label, getFormatDateFn) => {
202
227
  if (/^W\d+$/.test(label)) {
203
228
  return label;
204
229
  }
230
+ // Check for YYYY format (e.g., "2024", "2025")
231
+ if (/^\d{4}$/.test(label)) {
232
+ return label;
233
+ }
205
234
  }
206
235
 
207
236
  // Otherwise, try to parse and format it
@@ -209,7 +238,7 @@ export const formatDateAxis = (label, getFormatDateFn) => {
209
238
 
210
239
  // If first attempt fails, try parsing as ISO date string
211
240
  if (!date.isValid() && typeof label === 'string') {
212
- date = dayjs(label, ['YYYY-MM-DD', 'YYYY-MM', 'MMM YY', 'MMM YYYY', 'DD/MM'], true);
241
+ date = dayjs(label, ['YYYY-MM-DD', 'YYYY-MM', 'YYYY', 'MMM YY', 'MMM YYYY', 'DD/MM'], true);
213
242
  }
214
243
 
215
244
  // If it's a valid date, format it using getFormatDate