@easyteam/auto-scheduler-modal-ui 0.1.1 → 0.1.3

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/index.cjs CHANGED
@@ -905,6 +905,7 @@ var SCHEDULE_STEPS = [
905
905
  { title: "Finalizing schedule" }
906
906
  ];
907
907
  var STEP_INTERVAL_MS = 1200;
908
+ var POLL_INTERVAL_MS = 2500;
908
909
  var OPTIMIZING_STEP_INDEX = 2;
909
910
  var NETWORK_ERROR_FAILURE = {
910
911
  violatedConstraints: [
@@ -1360,7 +1361,7 @@ function buildRulesPayload(selectedConstraintIds, constraintValues, selectedOpti
1360
1361
  return { ruleIds: allSelectedRuleIds, ruleOverrides };
1361
1362
  }
1362
1363
  function toApiShift(shift, includeEmployeePreferences) {
1363
- var _a, _b;
1364
+ var _a;
1364
1365
  const startDateTime = "startDateTime" in shift ? shift.startDateTime : shift.startTime;
1365
1366
  const endDateTime = "endDateTime" in shift ? shift.endDateTime : shift.endTime;
1366
1367
  return {
@@ -1368,8 +1369,8 @@ function toApiShift(shift, includeEmployeePreferences) {
1368
1369
  startDateTime,
1369
1370
  endDateTime,
1370
1371
  ...shift.employeeId ? { employeeId: String(shift.employeeId) } : {},
1371
- requiredPosition: (_a = shift.requiredPosition) != null ? _a : void 0,
1372
- requiredCount: (_b = shift.requiredCount) != null ? _b : 1,
1372
+ requiredPosition: shift.requiredPosition || void 0,
1373
+ requiredCount: (_a = shift.requiredCount) != null ? _a : 1,
1373
1374
  location: shift.location ? {
1374
1375
  id: String(shift.location.id),
1375
1376
  timezone: "timezone" in shift.location ? shift.location.timezone : void 0,
@@ -1449,19 +1450,55 @@ async function getToken(fullUrl, headers) {
1449
1450
  });
1450
1451
  return (_a = tokenRes.data) == null ? void 0 : _a.token;
1451
1452
  }
1452
- async function solveSchedule(baseURL, payload, options) {
1453
+ async function submitSolveJob(baseURL, payload, options) {
1453
1454
  const cleanBaseUrl = baseURL.replace(/\/$/, "");
1454
1455
  const headers = {};
1455
1456
  if (options == null ? void 0 : options.authorization) {
1456
1457
  headers.Authorization = options.authorization;
1457
1458
  }
1458
1459
  const response = await axiosInstance.post(
1459
- `${cleanBaseUrl}/api/schedule/solve`,
1460
+ `${cleanBaseUrl}/api/schedule/solve-async`,
1460
1461
  payload,
1461
1462
  { headers: Object.keys(headers).length > 0 ? headers : void 0 }
1462
1463
  );
1463
1464
  return response.data;
1464
1465
  }
1466
+ async function getJobStatus(baseURL, jobId, options) {
1467
+ const cleanBaseUrl = baseURL.replace(/\/$/, "");
1468
+ const headers = {};
1469
+ if (options == null ? void 0 : options.authorization) {
1470
+ headers.Authorization = options.authorization;
1471
+ }
1472
+ const response = await axiosInstance.get(
1473
+ `${cleanBaseUrl}/api/schedule/status/${jobId}`,
1474
+ { headers: Object.keys(headers).length > 0 ? headers : void 0 }
1475
+ );
1476
+ return response.data;
1477
+ }
1478
+ async function getJobResult(baseURL, jobId, options) {
1479
+ const cleanBaseUrl = baseURL.replace(/\/$/, "");
1480
+ const headers = {};
1481
+ if (options == null ? void 0 : options.authorization) {
1482
+ headers.Authorization = options.authorization;
1483
+ }
1484
+ const response = await axiosInstance.get(
1485
+ `${cleanBaseUrl}/api/schedule/result/${jobId}`,
1486
+ { headers: Object.keys(headers).length > 0 ? headers : void 0 }
1487
+ );
1488
+ return response.data;
1489
+ }
1490
+ async function getJobSolution(baseURL, jobId, options) {
1491
+ const cleanBaseUrl = baseURL.replace(/\/$/, "");
1492
+ const headers = {};
1493
+ if (options == null ? void 0 : options.authorization) {
1494
+ headers.Authorization = options.authorization;
1495
+ }
1496
+ const response = await axiosInstance.get(
1497
+ `${cleanBaseUrl}/api/schedule/solution/${jobId}`,
1498
+ { headers: Object.keys(headers).length > 0 ? headers : void 0 }
1499
+ );
1500
+ return response.data;
1501
+ }
1465
1502
 
1466
1503
  // src/utils/violationMessages.ts
1467
1504
  var SYSTEM_CONSTRAINT_MESSAGES = {
@@ -1663,12 +1700,15 @@ function ViolatedConstraintsPanel({
1663
1700
  onSecondaryButtonClick,
1664
1701
  generateRecommendationsURLAndHeaders,
1665
1702
  styles: stylesProp,
1666
- styleOverrides
1703
+ styleOverrides,
1704
+ errorToast
1667
1705
  }) {
1668
1706
  const [showRecommendations, setShowRecommendations] = (0, import_react.useState)(false);
1669
1707
  const [isExpanded, setIsExpanded] = (0, import_react.useState)(true);
1670
1708
  const [recommendations, setRecommendations] = (0, import_react.useState)(null);
1671
1709
  const [isLoadingRecommendations, setIsLoadingRecommendations] = (0, import_react.useState)(false);
1710
+ const [isDisabledRecommendations, setIsDisabledRecommendations] = (0, import_react.useState)(false);
1711
+ const [recommendationsButtonText, setRecommendationsButtonText] = (0, import_react.useState)("Show recommendations");
1672
1712
  const styles = (0, import_react.useMemo)(() => {
1673
1713
  if (stylesProp) return stylesProp;
1674
1714
  return buildStyles(defaultStyles, styleOverrides);
@@ -1679,12 +1719,10 @@ function ViolatedConstraintsPanel({
1679
1719
  const displayRecommendations = recommendations !== null ? recommendations : violatedConstraints.recommendedFixes;
1680
1720
  const hasRecommendations = displayRecommendations.length > 0;
1681
1721
  const handleToggleRecommendations = (0, import_react.useCallback)(async () => {
1682
- var _a, _b;
1683
1722
  if (showRecommendations) {
1684
1723
  setShowRecommendations(false);
1685
1724
  return;
1686
1725
  }
1687
- setShowRecommendations(true);
1688
1726
  if (recommendations !== null) {
1689
1727
  return;
1690
1728
  }
@@ -1704,9 +1742,21 @@ function ViolatedConstraintsPanel({
1704
1742
  }
1705
1743
  }
1706
1744
  );
1707
- const data = (_b = (_a = recResponse.data) == null ? void 0 : _a.data) != null ? _b : recResponse.data;
1708
- if (data) {
1709
- setRecommendations(data.map((r) => r.recommendation));
1745
+ const { success, data, msg } = recResponse.data;
1746
+ const { isExpired, recommendations: recommendations2 } = data != null ? data : {};
1747
+ if (!success) {
1748
+ const errorMsg = msg || "Failed to fetch recommendations";
1749
+ errorToast == null ? void 0 : errorToast(errorMsg);
1750
+ if (isExpired) {
1751
+ setIsDisabledRecommendations(true);
1752
+ setRecommendationsButtonText("Daily limit reached");
1753
+ }
1754
+ return;
1755
+ }
1756
+ setShowRecommendations(true);
1757
+ const fetchedRecommendations = recommendations2;
1758
+ if (fetchedRecommendations) {
1759
+ setRecommendations(fetchedRecommendations.map((r) => r.recommendation));
1710
1760
  } else {
1711
1761
  setRecommendations([]);
1712
1762
  }
@@ -1721,7 +1771,8 @@ function ViolatedConstraintsPanel({
1721
1771
  recommendations,
1722
1772
  canFetchRecommendations,
1723
1773
  violatedConstraints.recommendationPayload,
1724
- generateRecommendationsURLAndHeaders
1774
+ generateRecommendationsURLAndHeaders,
1775
+ errorToast
1725
1776
  ]);
1726
1777
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react2.Box, { sx: styles.violationPanel, children: [
1727
1778
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react2.HStack, { sx: styles.violationHeaderRow, children: [
@@ -1773,9 +1824,9 @@ function ViolatedConstraintsPanel({
1773
1824
  variant: "outline",
1774
1825
  size: "xs",
1775
1826
  onClick: handleToggleRecommendations,
1776
- isDisabled: isLoadingRecommendations,
1827
+ isDisabled: isLoadingRecommendations || isDisabledRecommendations,
1777
1828
  sx: { ...styles.recommendationsToggle, minWidth: "140px" },
1778
- children: isLoadingRecommendations ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react2.Spinner, { size: "sm", thickness: "2px" }) : showRecommendations ? "Hide recommendations" : "Show recommendations"
1829
+ children: isLoadingRecommendations ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react2.Spinner, { size: "sm", thickness: "2px" }) : showRecommendations ? "Hide recommendations" : recommendationsButtonText
1779
1830
  }
1780
1831
  ) : null,
1781
1832
  showSecondaryButton && secondaryButtonTitle && onSecondaryButtonClick ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -1817,7 +1868,8 @@ function AutoSchedulerModal({
1817
1868
  getTokenURLAndHeaders,
1818
1869
  onSolution,
1819
1870
  getStoredConfigForLocations,
1820
- persistConfigForLocations
1871
+ persistConfigForLocations,
1872
+ errorToast
1821
1873
  }) {
1822
1874
  const baseTheme = (0, import_react4.useTheme)();
1823
1875
  const { chakraOverride, styleOverrides } = (0, import_react3.useMemo)(
@@ -1839,6 +1891,7 @@ function AutoSchedulerModal({
1839
1891
  const [lastFailure, setLastFailure] = (0, import_react3.useState)(null);
1840
1892
  const [selectedLocationIds, setSelectedLocationIds] = (0, import_react3.useState)([]);
1841
1893
  const timersRef = (0, import_react3.useRef)([]);
1894
+ const pollIntervalRef = (0, import_react3.useRef)(null);
1842
1895
  const runIdRef = (0, import_react3.useRef)(0);
1843
1896
  const selectedByJurisdiction = (0, import_react3.useMemo)(
1844
1897
  () => jurisdictions.map((jurisdiction) => ({
@@ -1853,6 +1906,11 @@ function AutoSchedulerModal({
1853
1906
  (total, jurisdiction) => total + jurisdiction.selectedLocations.length,
1854
1907
  0
1855
1908
  );
1909
+ const locationIdsWithOpenShifts = (0, import_react3.useMemo)(() => {
1910
+ return new Set(
1911
+ openShifts.filter((shift) => !shift.employeeId).map((shift) => shift.location.id)
1912
+ );
1913
+ }, [openShifts]);
1856
1914
  const selectedConstraints = (0, import_react3.useMemo)(
1857
1915
  () => HARD_CONSTRAINT_OPTIONS.filter((option) => selectedConstraintIds.includes(option.id)),
1858
1916
  [selectedConstraintIds]
@@ -1904,6 +1962,10 @@ function AutoSchedulerModal({
1904
1962
  const clearTimers = () => {
1905
1963
  timersRef.current.forEach((timerId) => clearTimeout(timerId));
1906
1964
  timersRef.current = [];
1965
+ if (pollIntervalRef.current) {
1966
+ clearInterval(pollIntervalRef.current);
1967
+ pollIntervalRef.current = null;
1968
+ }
1907
1969
  };
1908
1970
  const resetProgress = () => {
1909
1971
  setActiveStepIndex(0);
@@ -1953,6 +2015,12 @@ function AutoSchedulerModal({
1953
2015
  }
1954
2016
  }, [selectedLocationIds, initialConfig, getStoredConfigForLocations]);
1955
2017
  (0, import_react3.useEffect)(() => () => clearTimers(), []);
2018
+ (0, import_react3.useEffect)(() => {
2019
+ setSelectedLocationIds((previous) => {
2020
+ const filtered = previous.filter((id) => locationIdsWithOpenShifts.has(id));
2021
+ return filtered.length === previous.length ? previous : filtered;
2022
+ });
2023
+ }, [locationIdsWithOpenShifts]);
1956
2024
  const stepStatusForIndex = (index) => {
1957
2025
  if (index <= completedStepIndex) {
1958
2026
  return "done";
@@ -2037,7 +2105,6 @@ function AutoSchedulerModal({
2037
2105
  });
2038
2106
  const runId = resetBeforeSubmit();
2039
2107
  schedule(STEP_INTERVAL_MS, async () => {
2040
- var _a, _b;
2041
2108
  setCompletedStepIndex(0);
2042
2109
  setActiveStepIndex(1);
2043
2110
  try {
@@ -2045,51 +2112,47 @@ function AutoSchedulerModal({
2045
2112
  if (!token) {
2046
2113
  throw new Error("Failed to get auto-scheduler token");
2047
2114
  }
2048
- const response = await solveSchedule(
2049
- baseURL,
2050
- payload,
2051
- {
2052
- authorization: `Bearer ${token}`
2053
- }
2054
- );
2115
+ const auth = { authorization: `Bearer ${token}` };
2116
+ const { jobId } = await submitSolveJob(baseURL, payload, auth);
2055
2117
  if (runIdRef.current !== runId) {
2056
2118
  return;
2057
2119
  }
2058
- if (!response.explanation.isFeasible) {
2059
- const result = mapViolationsToResult(
2060
- (_a = response.explanation.violations) != null ? _a : [],
2061
- response.explanation.summary
2062
- );
2063
- const selectedLocations = jurisdictions.flatMap((j) => j.locations).filter(
2064
- (l) => selectedLocationIds.includes(l.id)
2065
- );
2066
- const recommendationPayload = generateRecommendationsURLAndHeaders ? {
2067
- context: {
2068
- selectedLocationIds,
2069
- selectedConstraintIds,
2070
- constraintValues,
2071
- selectedOptimizationIds,
2072
- optimizationValues,
2073
- timezone: (_b = selectedLocations[0]) == null ? void 0 : _b.timezone
2074
- },
2075
- locations: selectedLocations,
2076
- employees,
2077
- openShifts: openShifts.filter((s) => !s.employeeId),
2078
- assignedShifts: openShifts.filter((s) => Boolean(s.employeeId)),
2079
- timeOffs: timeOffs != null ? timeOffs : [],
2080
- explanation: response.explanation
2081
- } : void 0;
2082
- setLastFailure({
2083
- ...result,
2084
- recommendedFixes: [],
2085
- recommendationPayload
2086
- });
2087
- setScreen("failed");
2088
- } else {
2089
- setCompletedStepIndex(1);
2090
- setActiveStepIndex(OPTIMIZING_STEP_INDEX);
2091
- afterSuccessSteps(runId, response);
2092
- }
2120
+ setCompletedStepIndex(1);
2121
+ setActiveStepIndex(OPTIMIZING_STEP_INDEX);
2122
+ const pollForResult = async () => {
2123
+ if (runIdRef.current !== runId) return;
2124
+ try {
2125
+ const statusRes = await getJobStatus(baseURL, jobId, auth);
2126
+ if (runIdRef.current !== runId) return;
2127
+ if (statusRes.status === "NOT_SOLVING" || statusRes.status === "TERMINATED_EARLY") {
2128
+ if (pollIntervalRef.current) {
2129
+ clearInterval(pollIntervalRef.current);
2130
+ pollIntervalRef.current = null;
2131
+ }
2132
+ const response = await getJobResult(baseURL, jobId, auth);
2133
+ if (runIdRef.current !== runId) return;
2134
+ let responseForHandling = response;
2135
+ if (response.explanation.isFeasible) {
2136
+ const solution = await getJobSolution(baseURL, jobId, auth);
2137
+ if (runIdRef.current !== runId) return;
2138
+ responseForHandling = { ...response, solution };
2139
+ }
2140
+ handleSolveResult(runId, responseForHandling);
2141
+ return;
2142
+ }
2143
+ } catch (err) {
2144
+ if (runIdRef.current !== runId) return;
2145
+ if (pollIntervalRef.current) {
2146
+ clearInterval(pollIntervalRef.current);
2147
+ pollIntervalRef.current = null;
2148
+ }
2149
+ console.error(err);
2150
+ setLastFailure(NETWORK_ERROR_FAILURE);
2151
+ setScreen("failed");
2152
+ }
2153
+ };
2154
+ pollForResult();
2155
+ pollIntervalRef.current = setInterval(pollForResult, POLL_INTERVAL_MS);
2093
2156
  } catch (err) {
2094
2157
  if (runIdRef.current !== runId) {
2095
2158
  return;
@@ -2100,11 +2163,50 @@ function AutoSchedulerModal({
2100
2163
  }
2101
2164
  }, runId);
2102
2165
  };
2166
+ const handleSolveResult = (runId, response) => {
2167
+ var _a, _b;
2168
+ if (!response.explanation.isFeasible) {
2169
+ const result = mapViolationsToResult(
2170
+ (_a = response.explanation.violations) != null ? _a : [],
2171
+ response.explanation.summary
2172
+ );
2173
+ const selectedLocations = jurisdictions.flatMap((j) => j.locations).filter(
2174
+ (l) => selectedLocationIds.includes(l.id)
2175
+ );
2176
+ const recommendationPayload = generateRecommendationsURLAndHeaders ? {
2177
+ context: {
2178
+ selectedLocationIds,
2179
+ selectedConstraintIds,
2180
+ constraintValues,
2181
+ selectedOptimizationIds,
2182
+ optimizationValues,
2183
+ timezone: (_b = selectedLocations[0]) == null ? void 0 : _b.timezone
2184
+ },
2185
+ locations: selectedLocations,
2186
+ employees,
2187
+ openShifts: openShifts.filter((s) => !s.employeeId),
2188
+ assignedShifts: openShifts.filter((s) => Boolean(s.employeeId)),
2189
+ timeOffs: timeOffs != null ? timeOffs : [],
2190
+ explanation: response.explanation
2191
+ } : void 0;
2192
+ setLastFailure({
2193
+ ...result,
2194
+ recommendedFixes: [],
2195
+ recommendationPayload
2196
+ });
2197
+ setScreen("failed");
2198
+ } else {
2199
+ afterSuccessSteps(runId, response);
2200
+ }
2201
+ };
2103
2202
  const handleAdjustConstraints = () => {
2104
2203
  clearTimers();
2105
2204
  setScreen("configure");
2106
2205
  };
2107
2206
  const handleToggleLocation = (locationId) => {
2207
+ if (!locationIdsWithOpenShifts.has(locationId)) {
2208
+ return;
2209
+ }
2108
2210
  if (selectedLocationIds.includes(locationId)) {
2109
2211
  setSelectedLocationIds(selectedLocationIds.filter((id) => id !== locationId));
2110
2212
  return;
@@ -2168,6 +2270,7 @@ function AutoSchedulerModal({
2168
2270
  violatedConstraints: lastFailure,
2169
2271
  title: violationTitle,
2170
2272
  subtitle: "",
2273
+ errorToast,
2171
2274
  generateRecommendationsURLAndHeaders,
2172
2275
  styles
2173
2276
  }
@@ -2227,13 +2330,18 @@ function AutoSchedulerModal({
2227
2330
  import_react4.HStack,
2228
2331
  {
2229
2332
  as: "label",
2230
- sx: { ...styles.optionRow, cursor: "pointer" },
2333
+ sx: {
2334
+ ...styles.optionRow,
2335
+ cursor: locationIdsWithOpenShifts.has(location.id) ? "pointer" : "not-allowed",
2336
+ opacity: locationIdsWithOpenShifts.has(location.id) ? 1 : 0.55
2337
+ },
2231
2338
  children: [
2232
2339
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2233
2340
  import_react4.Checkbox,
2234
2341
  {
2235
2342
  id: `location-${location.id}`,
2236
2343
  isChecked: selectedLocationIds.includes(location.id),
2344
+ isDisabled: !locationIdsWithOpenShifts.has(location.id),
2237
2345
  onChange: () => handleToggleLocation(location.id)
2238
2346
  }
2239
2347
  ),