@easyteam/auto-scheduler-modal-ui 0.1.2 → 0.1.4

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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  // src/AutoSchedulerModal.tsx
4
- import { useEffect, useMemo as useMemo2, useRef, useState as useState2 } from "react";
4
+ import { cloneElement, isValidElement, useEffect, useMemo as useMemo2, useRef, useState as useState2 } from "react";
5
5
  import {
6
6
  Badge,
7
7
  Box as Box2,
@@ -24,11 +24,22 @@ import {
24
24
  PopoverBody,
25
25
  PopoverContent,
26
26
  PopoverTrigger,
27
+ Portal,
27
28
  Spinner as Spinner2,
28
29
  Stack as Stack2,
29
30
  Text as Text2,
30
31
  useTheme
31
32
  } from "@chakra-ui/react";
33
+ import { Drawer as EtDrawer } from "@easyteam/web-ui";
34
+
35
+ // src/types.ts
36
+ var InfeasibleScheduleError = class extends Error {
37
+ constructor(failure) {
38
+ super("Schedule could not be created due to constraint violations");
39
+ this.name = "InfeasibleScheduleError";
40
+ this.failure = failure;
41
+ }
42
+ };
32
43
 
33
44
  // src/icons/AutoSchedulerIcons.tsx
34
45
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -165,19 +176,15 @@ function CheckIcon(props) {
165
176
  "svg",
166
177
  {
167
178
  "aria-hidden": "true",
168
- viewBox: "0 0 20 20",
169
- fill: "none",
170
- width: "20",
171
- height: "20",
179
+ viewBox: "0 0 24 24",
180
+ width: "24",
181
+ height: "24",
172
182
  ...props,
173
183
  children: /* @__PURE__ */ jsx(
174
184
  "path",
175
185
  {
176
- d: "M5.5 10.5l3 3 6-6",
177
- stroke: "currentColor",
178
- strokeWidth: "1.6",
179
- strokeLinecap: "round",
180
- strokeLinejoin: "round"
186
+ fill: "currentColor",
187
+ d: "M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"
181
188
  }
182
189
  )
183
190
  }
@@ -699,7 +706,7 @@ var defaultStyles = {
699
706
  },
700
707
  stepIconDone: {
701
708
  border: "none",
702
- bg: "#008060",
709
+ bg: "#5338ff",
703
710
  color: "white",
704
711
  borderRadius: "10px"
705
712
  },
@@ -1235,6 +1242,9 @@ var UI_TO_BACKEND_RULE_ID = {
1235
1242
  maximize_sales: "maximize-sales-performance",
1236
1243
  staff_preferences: "staff-open-shift-preferences"
1237
1244
  };
1245
+ var MAXIMIZE_CONVERSION_RATE_RULE_ID = "maximize-conversion-rate";
1246
+ var TRAFFIC_FORECAST_INCENTIVE_RULE_ID = "traffic-forecast-incentive";
1247
+ var SALES_OPTIMIZATION_UI_IDS = /* @__PURE__ */ new Set(["maximize_sales", "cost_to_sales_ratio"]);
1238
1248
  var BACKEND_TO_UI_RULE_ID = Object.fromEntries(
1239
1249
  Object.entries(UI_TO_BACKEND_RULE_ID).map(([ui, backend]) => [backend, ui])
1240
1250
  );
@@ -1311,11 +1321,24 @@ function hasAnyParameterOverrides(selectedConstraintIds, constraintValues, selec
1311
1321
  }
1312
1322
  return false;
1313
1323
  }
1314
- function buildRulesPayload(selectedConstraintIds, constraintValues, selectedOptimizationIds, optimizationValues) {
1324
+ function appendSiblingRuleId(ruleIds, siblingId) {
1325
+ if (!ruleIds.includes(siblingId)) {
1326
+ ruleIds.push(siblingId);
1327
+ }
1328
+ }
1329
+ function buildRulesPayload(selectedConstraintIds, constraintValues, selectedOptimizationIds, optimizationValues, siblings = {}) {
1315
1330
  const allSelectedRuleIds = [
1316
1331
  ...selectedConstraintIds.map(toBackendRuleId),
1317
1332
  ...selectedOptimizationIds.map(toBackendRuleId)
1318
1333
  ].filter((id) => Boolean(id));
1334
+ const salesOptSelected = selectedOptimizationIds.some((id) => SALES_OPTIMIZATION_UI_IDS.has(id));
1335
+ const maximizeSalesSelected = selectedOptimizationIds.includes("maximize_sales");
1336
+ if (siblings.includeConversionRate && salesOptSelected) {
1337
+ appendSiblingRuleId(allSelectedRuleIds, MAXIMIZE_CONVERSION_RATE_RULE_ID);
1338
+ }
1339
+ if (siblings.includeTrafficIncentive && maximizeSalesSelected) {
1340
+ appendSiblingRuleId(allSelectedRuleIds, TRAFFIC_FORECAST_INCENTIVE_RULE_ID);
1341
+ }
1319
1342
  if (allSelectedRuleIds.length === 0) {
1320
1343
  return {};
1321
1344
  }
@@ -1345,7 +1368,7 @@ function buildRulesPayload(selectedConstraintIds, constraintValues, selectedOpti
1345
1368
  return { ruleIds: allSelectedRuleIds, ruleOverrides };
1346
1369
  }
1347
1370
  function toApiShift(shift, includeEmployeePreferences) {
1348
- var _a, _b;
1371
+ var _a;
1349
1372
  const startDateTime = "startDateTime" in shift ? shift.startDateTime : shift.startTime;
1350
1373
  const endDateTime = "endDateTime" in shift ? shift.endDateTime : shift.endTime;
1351
1374
  return {
@@ -1353,8 +1376,8 @@ function toApiShift(shift, includeEmployeePreferences) {
1353
1376
  startDateTime,
1354
1377
  endDateTime,
1355
1378
  ...shift.employeeId ? { employeeId: String(shift.employeeId) } : {},
1356
- requiredPosition: (_a = shift.requiredPosition) != null ? _a : void 0,
1357
- requiredCount: (_b = shift.requiredCount) != null ? _b : 1,
1379
+ requiredPosition: shift.requiredPosition || void 0,
1380
+ requiredCount: (_a = shift.requiredCount) != null ? _a : 1,
1358
1381
  location: shift.location ? {
1359
1382
  id: String(shift.location.id),
1360
1383
  timezone: "timezone" in shift.location ? shift.location.timezone : void 0,
@@ -1384,9 +1407,25 @@ function toApiLocation(location) {
1384
1407
  id: String(location.id),
1385
1408
  timezone: (_a = location.timezone) != null ? _a : void 0,
1386
1409
  jurisdiction: (_b = location.jurisdiction) != null ? _b : void 0,
1387
- tags: "tags" in location ? location.tags : void 0
1410
+ tags: "tags" in location ? location.tags : void 0,
1411
+ properties: location.properties ? location.properties : void 0
1388
1412
  };
1389
1413
  }
1414
+ function employeeHasConversionRate(employee) {
1415
+ var _a;
1416
+ const value = (_a = employee.properties) == null ? void 0 : _a.conversionRate;
1417
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
1418
+ }
1419
+ function employeeTouchesSelectedLocations(employee, selectedLocationIds) {
1420
+ var _a;
1421
+ const ids = (_a = employee.locationIds) != null ? _a : [];
1422
+ return ids.some((id) => selectedLocationIds.includes(String(id)));
1423
+ }
1424
+ function hasConversionRateInSelectedLocations(employees, selectedLocationIds) {
1425
+ return employees.some(
1426
+ (employee) => employeeTouchesSelectedLocations(employee, selectedLocationIds) && employeeHasConversionRate(employee)
1427
+ );
1428
+ }
1390
1429
  function buildSchedulePayload(input) {
1391
1430
  const {
1392
1431
  employees,
@@ -1397,7 +1436,11 @@ function buildSchedulePayload(input) {
1397
1436
  selectedConstraintIds,
1398
1437
  constraintValues,
1399
1438
  selectedOptimizationIds,
1400
- optimizationValues
1439
+ optimizationValues,
1440
+ trafficForecast,
1441
+ constraintDefinitionIds,
1442
+ constraintDefinitions,
1443
+ organizationId
1401
1444
  } = input;
1402
1445
  const locationsMap = /* @__PURE__ */ new Map();
1403
1446
  for (const j of jurisdictions) {
@@ -1406,19 +1449,45 @@ function buildSchedulePayload(input) {
1406
1449
  }
1407
1450
  }
1408
1451
  const selectedLocations = selectedLocationIds.map((id) => locationsMap.get(id)).filter((loc) => Boolean(loc));
1452
+ const scopedOpenShifts = openShifts.filter(
1453
+ (shift) => {
1454
+ var _a;
1455
+ return selectedLocationIds.includes(String((_a = shift.location) == null ? void 0 : _a.id));
1456
+ }
1457
+ );
1458
+ const includeConversionRate = hasConversionRateInSelectedLocations(
1459
+ employees,
1460
+ selectedLocationIds
1461
+ );
1462
+ const scopedTrafficForecast = (trafficForecast != null ? trafficForecast : []).filter(
1463
+ (entry) => selectedLocationIds.includes(String(entry.locationId))
1464
+ );
1465
+ const includeTrafficIncentive = scopedTrafficForecast.length > 0;
1409
1466
  const rules = buildRulesPayload(
1410
1467
  selectedConstraintIds,
1411
1468
  constraintValues,
1412
1469
  selectedOptimizationIds,
1413
- optimizationValues
1470
+ optimizationValues,
1471
+ { includeConversionRate, includeTrafficIncentive }
1414
1472
  );
1415
1473
  const includeEmployeePreferences = selectedOptimizationIds.includes("staff_preferences");
1474
+ const hasIds = Boolean(constraintDefinitionIds && constraintDefinitionIds.length > 0);
1475
+ const hasDefs = Boolean(constraintDefinitions && constraintDefinitions.length > 0);
1476
+ const hasRules = hasIds || hasDefs;
1477
+ if (hasIds && !organizationId) {
1478
+ throw new Error("organizationId is required when constraintDefinitionIds are provided");
1479
+ }
1480
+ const trafficRuleActive = includeTrafficIncentive && selectedOptimizationIds.includes("maximize_sales");
1416
1481
  return {
1417
1482
  employees: employees.map(toApiEmployee),
1418
- shifts: openShifts.map((shift) => toApiShift(shift, includeEmployeePreferences)),
1483
+ shifts: scopedOpenShifts.map((shift) => toApiShift(shift, includeEmployeePreferences)),
1419
1484
  locations: selectedLocations.map(toApiLocation),
1420
1485
  ...timeOffs && timeOffs.length > 0 ? { timeOffs } : {},
1421
- ...rules
1486
+ ...rules,
1487
+ ...trafficRuleActive ? { trafficForecast: scopedTrafficForecast } : {},
1488
+ ...hasIds ? { constraintDefinitionIds } : {},
1489
+ ...hasDefs ? { constraintDefinitions } : {},
1490
+ ...hasRules && organizationId ? { organizationId } : {}
1422
1491
  };
1423
1492
  }
1424
1493
 
@@ -1441,7 +1510,7 @@ async function submitSolveJob(baseURL, payload, options) {
1441
1510
  headers.Authorization = options.authorization;
1442
1511
  }
1443
1512
  const response = await axiosInstance.post(
1444
- `${cleanBaseUrl}/api/schedule/solve-async`,
1513
+ withHorizonQuery(`${cleanBaseUrl}/api/schedule/solve-async`, options),
1445
1514
  payload,
1446
1515
  { headers: Object.keys(headers).length > 0 ? headers : void 0 }
1447
1516
  );
@@ -1483,6 +1552,16 @@ async function getJobSolution(baseURL, jobId, options) {
1483
1552
  );
1484
1553
  return response.data;
1485
1554
  }
1555
+ function withHorizonQuery(url, options) {
1556
+ if (!(options == null ? void 0 : options.horizonStartDate) || !(options == null ? void 0 : options.horizonEndDate)) {
1557
+ return url;
1558
+ }
1559
+ const params = new URLSearchParams({
1560
+ dateRangeStart: options.horizonStartDate,
1561
+ dateRangeEnd: options.horizonEndDate
1562
+ });
1563
+ return `${url}?${params.toString()}`;
1564
+ }
1486
1565
 
1487
1566
  // src/utils/violationMessages.ts
1488
1567
  var SYSTEM_CONSTRAINT_MESSAGES = {
@@ -1630,17 +1709,14 @@ function getRuleBasedViolationMessage(ruleId, constraintName, description) {
1630
1709
  }
1631
1710
 
1632
1711
  // src/utils/mapViolationsToResult.ts
1633
- function mapViolationsToResult(violations, summary) {
1712
+ function mapViolationsToResult(violations, summary, customMessages) {
1713
+ var _a;
1634
1714
  const hardViolations = violations.filter((v) => v.isHardViolation === true);
1635
1715
  const seenTitles = /* @__PURE__ */ new Set();
1636
1716
  const violatedConstraints = [];
1637
1717
  for (const v of hardViolations) {
1638
1718
  const hasRuleId = v.ruleId != null && v.ruleId !== void 0 && String(v.ruleId).trim() !== "";
1639
- const msg = hasRuleId ? getRuleBasedViolationMessage(
1640
- v.ruleId,
1641
- v.constraintName,
1642
- v.description
1643
- ) : getSystemConstraintMessage(v.constraintName, v.description);
1719
+ const msg = hasRuleId ? (_a = customMessages == null ? void 0 : customMessages[v.ruleId]) != null ? _a : getRuleBasedViolationMessage(v.ruleId, v.constraintName, v.description) : getSystemConstraintMessage(v.constraintName, v.description);
1644
1720
  if (!seenTitles.has(msg.title)) {
1645
1721
  seenTitles.add(msg.title);
1646
1722
  violatedConstraints.push(msg);
@@ -1692,12 +1768,15 @@ function ViolatedConstraintsPanel({
1692
1768
  onSecondaryButtonClick,
1693
1769
  generateRecommendationsURLAndHeaders,
1694
1770
  styles: stylesProp,
1695
- styleOverrides
1771
+ styleOverrides,
1772
+ errorToast
1696
1773
  }) {
1697
1774
  const [showRecommendations, setShowRecommendations] = useState(false);
1698
1775
  const [isExpanded, setIsExpanded] = useState(true);
1699
1776
  const [recommendations, setRecommendations] = useState(null);
1700
1777
  const [isLoadingRecommendations, setIsLoadingRecommendations] = useState(false);
1778
+ const [isDisabledRecommendations, setIsDisabledRecommendations] = useState(false);
1779
+ const [recommendationsButtonText, setRecommendationsButtonText] = useState("Show recommendations");
1701
1780
  const styles = useMemo(() => {
1702
1781
  if (stylesProp) return stylesProp;
1703
1782
  return buildStyles(defaultStyles, styleOverrides);
@@ -1708,13 +1787,12 @@ function ViolatedConstraintsPanel({
1708
1787
  const displayRecommendations = recommendations !== null ? recommendations : violatedConstraints.recommendedFixes;
1709
1788
  const hasRecommendations = displayRecommendations.length > 0;
1710
1789
  const handleToggleRecommendations = useCallback(async () => {
1711
- var _a, _b;
1712
1790
  if (showRecommendations) {
1713
1791
  setShowRecommendations(false);
1714
1792
  return;
1715
1793
  }
1716
- setShowRecommendations(true);
1717
1794
  if (recommendations !== null) {
1795
+ setShowRecommendations(true);
1718
1796
  return;
1719
1797
  }
1720
1798
  if (!canFetchRecommendations) {
@@ -1733,9 +1811,21 @@ function ViolatedConstraintsPanel({
1733
1811
  }
1734
1812
  }
1735
1813
  );
1736
- const data = (_b = (_a = recResponse.data) == null ? void 0 : _a.data) != null ? _b : recResponse.data;
1737
- if (data) {
1738
- setRecommendations(data.map((r) => r.recommendation));
1814
+ const { success, data, msg } = recResponse.data;
1815
+ const { isExpired, recommendations: recommendations2 } = data != null ? data : {};
1816
+ if (!success) {
1817
+ const errorMsg = msg || "Failed to fetch recommendations";
1818
+ errorToast == null ? void 0 : errorToast(errorMsg);
1819
+ if (isExpired) {
1820
+ setIsDisabledRecommendations(true);
1821
+ setRecommendationsButtonText("Daily limit reached");
1822
+ }
1823
+ return;
1824
+ }
1825
+ setShowRecommendations(true);
1826
+ const fetchedRecommendations = recommendations2;
1827
+ if (fetchedRecommendations) {
1828
+ setRecommendations(fetchedRecommendations.map((r) => r.recommendation));
1739
1829
  } else {
1740
1830
  setRecommendations([]);
1741
1831
  }
@@ -1750,7 +1840,8 @@ function ViolatedConstraintsPanel({
1750
1840
  recommendations,
1751
1841
  canFetchRecommendations,
1752
1842
  violatedConstraints.recommendationPayload,
1753
- generateRecommendationsURLAndHeaders
1843
+ generateRecommendationsURLAndHeaders,
1844
+ errorToast
1754
1845
  ]);
1755
1846
  return /* @__PURE__ */ jsxs2(Box, { sx: styles.violationPanel, children: [
1756
1847
  /* @__PURE__ */ jsxs2(HStack, { sx: styles.violationHeaderRow, children: [
@@ -1802,9 +1893,9 @@ function ViolatedConstraintsPanel({
1802
1893
  variant: "outline",
1803
1894
  size: "xs",
1804
1895
  onClick: handleToggleRecommendations,
1805
- isDisabled: isLoadingRecommendations,
1896
+ isDisabled: isLoadingRecommendations || isDisabledRecommendations,
1806
1897
  sx: { ...styles.recommendationsToggle, minWidth: "140px" },
1807
- children: isLoadingRecommendations ? /* @__PURE__ */ jsx2(Spinner, { size: "sm", thickness: "2px" }) : showRecommendations ? "Hide recommendations" : "Show recommendations"
1898
+ children: isLoadingRecommendations ? /* @__PURE__ */ jsx2(Spinner, { size: "sm", thickness: "2px" }) : showRecommendations ? "Hide recommendations" : recommendationsButtonText
1808
1899
  }
1809
1900
  ) : null,
1810
1901
  showSecondaryButton && secondaryButtonTitle && onSecondaryButtonClick ? /* @__PURE__ */ jsx2(
@@ -1822,13 +1913,78 @@ function ViolatedConstraintsPanel({
1822
1913
  ] });
1823
1914
  }
1824
1915
 
1916
+ // src/weekDates.ts
1917
+ function parseCalendarYmd(ymd) {
1918
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(ymd).trim());
1919
+ if (!m) {
1920
+ return /* @__PURE__ */ new Date(NaN);
1921
+ }
1922
+ return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
1923
+ }
1924
+ function formatCalendarYmd(d) {
1925
+ const y = d.getFullYear();
1926
+ const mo = String(d.getMonth() + 1).padStart(2, "0");
1927
+ const da = String(d.getDate()).padStart(2, "0");
1928
+ return `${y}-${mo}-${da}`;
1929
+ }
1930
+ function getSequentialWeekDatesFromWeekStart(weekStartYmd) {
1931
+ const base = parseCalendarYmd(weekStartYmd);
1932
+ if (Number.isNaN(base.getTime())) return [];
1933
+ const out = [];
1934
+ for (let i = 0; i < 7; i++) {
1935
+ const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + i);
1936
+ out.push(formatCalendarYmd(d));
1937
+ }
1938
+ return out;
1939
+ }
1940
+ function weekdayTwoLetterFromYmd(ymd) {
1941
+ var _a;
1942
+ const labels = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
1943
+ const d = parseCalendarYmd(ymd);
1944
+ if (Number.isNaN(d.getTime())) return "?";
1945
+ return (_a = labels[d.getDay()]) != null ? _a : "?";
1946
+ }
1947
+ function defaultSelectedScheduleDates(orderedWeekYmds, todayYmd) {
1948
+ return orderedWeekYmds.filter((d) => d >= todayYmd);
1949
+ }
1950
+ function todayCalendarYmdInTimezone(timezone) {
1951
+ const fallback = formatCalendarYmd(/* @__PURE__ */ new Date());
1952
+ const tz = timezone == null ? void 0 : timezone.trim();
1953
+ if (!tz) return fallback;
1954
+ try {
1955
+ const parts = new Intl.DateTimeFormat("en-CA", {
1956
+ timeZone: tz,
1957
+ year: "numeric",
1958
+ month: "2-digit",
1959
+ day: "2-digit"
1960
+ }).formatToParts(/* @__PURE__ */ new Date());
1961
+ const valueByType = new Map(parts.map((part) => [part.type, part.value]));
1962
+ const year = valueByType.get("year");
1963
+ const month = valueByType.get("month");
1964
+ const day = valueByType.get("day");
1965
+ return year && month && day ? `${year}-${month}-${day}` : fallback;
1966
+ } catch {
1967
+ return fallback;
1968
+ }
1969
+ }
1970
+
1825
1971
  // src/AutoSchedulerModal.tsx
1826
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1972
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1973
+ function resolveOptimizationFooter(footer, key) {
1974
+ if (!footer) return null;
1975
+ const node = typeof footer === "function" ? footer() : footer;
1976
+ if (isValidElement(node)) return cloneElement(node, { key });
1977
+ return node;
1978
+ }
1979
+ var DRAWER_WIDTH_MAP = { sm: "448px", md: "578px", lg: "600px" };
1827
1980
  function AutoSchedulerModal({
1981
+ displayMode,
1982
+ drawerSize = "lg",
1828
1983
  baseURL,
1829
1984
  isOpen,
1830
1985
  title = "Auto-scheduling",
1831
- bannerText,
1986
+ bannerTitle = "Create a smarter retail schedule in seconds.",
1987
+ bannerText = "AI assigns open shifts using your rules, availability, and optimization priorities while keeping existing\n assigned shifts in place.",
1832
1988
  jurisdictions,
1833
1989
  openShifts,
1834
1990
  employees,
@@ -1846,22 +2002,79 @@ function AutoSchedulerModal({
1846
2002
  getTokenURLAndHeaders,
1847
2003
  onSolution,
1848
2004
  getStoredConfigForLocations,
1849
- persistConfigForLocations
2005
+ persistConfigForLocations,
2006
+ errorToast,
2007
+ organizationId,
2008
+ constraintDefinitionIds,
2009
+ constraintDefinitions,
2010
+ hardConstraintOptions,
2011
+ softConstraintGroups,
2012
+ optimizationFooter,
2013
+ trafficForecast,
2014
+ builtinConstraintOptions,
2015
+ subLocations,
2016
+ showSubLocations,
2017
+ teams,
2018
+ showTeams,
2019
+ onSelectionChange,
2020
+ onGenerate,
2021
+ onGenerateSolution,
2022
+ solveStatus,
2023
+ scheduleWeekStartYmd,
2024
+ todayCalendarYmd,
2025
+ evaluationTimezone
1850
2026
  }) {
1851
2027
  const baseTheme = useTheme();
1852
- const { chakraOverride, styleOverrides } = useMemo2(
1853
- () => resolveThemeParts(theme),
1854
- [theme]
1855
- );
2028
+ const { chakraOverride, styleOverrides } = useMemo2(() => resolveThemeParts(theme), [theme]);
1856
2029
  const mergedTheme = useMemo2(
1857
2030
  () => mergeThemeWithBase(baseTheme, chakraOverride),
1858
2031
  [baseTheme, chakraOverride]
1859
2032
  );
1860
2033
  const styles = useMemo2(() => buildStyles(defaultStyles, styleOverrides), [styleOverrides]);
2034
+ const effectiveHardConstraintOptions = useMemo2(() => {
2035
+ const base = hardConstraintOptions != null ? hardConstraintOptions : HARD_CONSTRAINT_OPTIONS;
2036
+ return base.map((opt) => {
2037
+ var _a, _b, _c, _d;
2038
+ return {
2039
+ ...opt,
2040
+ defaultValue: (_a = opt.defaultValue) != null ? _a : HARD_CONSTRAINT_DEFAULTS[opt.id],
2041
+ suffix: (_b = opt.suffix) != null ? _b : opt.id === "max_work_days_per_week" ? "days" : "hours",
2042
+ min: (_c = opt.min) != null ? _c : opt.id === "max_work_days_per_week" ? 1 : void 0,
2043
+ max: (_d = opt.max) != null ? _d : opt.id === "max_work_days_per_week" ? 6 : void 0
2044
+ };
2045
+ });
2046
+ }, [hardConstraintOptions]);
2047
+ const effectiveSoftConstraintGroups = useMemo2(
2048
+ () => softConstraintGroups != null ? softConstraintGroups : OPTIMIZATION_OPTION_GROUPS,
2049
+ [softConstraintGroups]
2050
+ );
2051
+ const effectiveBuiltinOptions = useMemo2(
2052
+ () => builtinConstraintOptions != null ? builtinConstraintOptions : [],
2053
+ [builtinConstraintOptions]
2054
+ );
2055
+ const defaultSelectedBuiltinIds = useMemo2(
2056
+ () => effectiveBuiltinOptions.filter((option) => option.defaultSelected).map((option) => option.id),
2057
+ [effectiveBuiltinOptions]
2058
+ );
2059
+ const dynamicViolationMessages = useMemo2(() => {
2060
+ const map = {};
2061
+ for (const opt of effectiveHardConstraintOptions) {
2062
+ map[opt.id] = { title: opt.name, detail: opt.description || opt.name };
2063
+ }
2064
+ for (const group of effectiveSoftConstraintGroups) {
2065
+ for (const opt of group.options) {
2066
+ map[opt.id] = { title: opt.title, detail: opt.subtitle || opt.title };
2067
+ }
2068
+ }
2069
+ return map;
2070
+ }, [effectiveHardConstraintOptions, effectiveSoftConstraintGroups]);
2071
+ const [selectedSubLocationIds, setSelectedSubLocationIds] = useState2([]);
2072
+ const [selectedTeamIds, setSelectedTeamIds] = useState2([]);
1861
2073
  const [selectedConstraintIds, setSelectedConstraintIds] = useState2([]);
1862
2074
  const [constraintValues, setConstraintValues] = useState2({});
1863
2075
  const [selectedOptimizationIds, setSelectedOptimizationIds] = useState2([]);
1864
2076
  const [optimizationValues, setOptimizationValues] = useState2({});
2077
+ const [selectedScheduleDates, setSelectedScheduleDates] = useState2([]);
1865
2078
  const [screen, setScreen] = useState2("configure");
1866
2079
  const [activeStepIndex, setActiveStepIndex] = useState2(0);
1867
2080
  const [completedStepIndex, setCompletedStepIndex] = useState2(-1);
@@ -1870,12 +2083,19 @@ function AutoSchedulerModal({
1870
2083
  const timersRef = useRef([]);
1871
2084
  const pollIntervalRef = useRef(null);
1872
2085
  const runIdRef = useRef(0);
2086
+ const weekDatesOrdered = useMemo2(() => {
2087
+ if (!scheduleWeekStartYmd) return [];
2088
+ return getSequentialWeekDatesFromWeekStart(scheduleWeekStartYmd);
2089
+ }, [scheduleWeekStartYmd]);
2090
+ const toggleScheduleDate = (ymd) => {
2091
+ setSelectedScheduleDates(
2092
+ (prev) => prev.includes(ymd) ? prev.filter((d) => d !== ymd) : [...prev, ymd].sort()
2093
+ );
2094
+ };
1873
2095
  const selectedByJurisdiction = useMemo2(
1874
2096
  () => jurisdictions.map((jurisdiction) => ({
1875
2097
  ...jurisdiction,
1876
- selectedLocations: jurisdiction.locations.filter(
1877
- (location) => selectedLocationIds.includes(location.id)
1878
- )
2098
+ selectedLocations: jurisdiction.locations.filter((location) => selectedLocationIds.includes(location.id))
1879
2099
  })).filter((jurisdiction) => jurisdiction.selectedLocations.length > 0),
1880
2100
  [jurisdictions, selectedLocationIds]
1881
2101
  );
@@ -1883,39 +2103,85 @@ function AutoSchedulerModal({
1883
2103
  (total, jurisdiction) => total + jurisdiction.selectedLocations.length,
1884
2104
  0
1885
2105
  );
2106
+ const locationIdsWithOpenShifts = useMemo2(() => {
2107
+ return new Set(openShifts.filter((shift) => !shift.employeeId).map((shift) => shift.location.id));
2108
+ }, [openShifts]);
2109
+ const filteredSubLocations = useMemo2(
2110
+ () => {
2111
+ var _a;
2112
+ return (_a = subLocations == null ? void 0 : subLocations.filter((sl) => selectedLocationIds.includes(sl.parentId))) != null ? _a : [];
2113
+ },
2114
+ [subLocations, selectedLocationIds]
2115
+ );
2116
+ const selectedLocationTimezone = useMemo2(() => {
2117
+ var _a, _b;
2118
+ const allLocations = jurisdictions.flatMap((jurisdiction) => jurisdiction.locations);
2119
+ for (const id of selectedLocationIds) {
2120
+ const timezone = (_b = (_a = allLocations.find((location) => location.id === id)) == null ? void 0 : _a.timezone) == null ? void 0 : _b.trim();
2121
+ if (timezone) return timezone;
2122
+ }
2123
+ return (evaluationTimezone == null ? void 0 : evaluationTimezone.trim()) || "UTC";
2124
+ }, [evaluationTimezone, jurisdictions, selectedLocationIds]);
2125
+ const effectiveTodayCalendarYmd = useMemo2(() => {
2126
+ return todayCalendarYmd != null ? todayCalendarYmd : todayCalendarYmdInTimezone(selectedLocationTimezone);
2127
+ }, [selectedLocationTimezone, todayCalendarYmd]);
1886
2128
  const selectedConstraints = useMemo2(
1887
- () => HARD_CONSTRAINT_OPTIONS.filter((option) => selectedConstraintIds.includes(option.id)),
1888
- [selectedConstraintIds]
2129
+ () => effectiveHardConstraintOptions.filter((option) => selectedConstraintIds.includes(option.id)),
2130
+ [effectiveHardConstraintOptions, selectedConstraintIds]
2131
+ );
2132
+ const selectedConstraintDisplayOptions = useMemo2(
2133
+ () => effectiveHardConstraintOptions.filter(
2134
+ (option) => selectedConstraintIds.includes(option.id) || option.readOnly && option.defaultSelected
2135
+ ),
2136
+ [effectiveHardConstraintOptions, selectedConstraintIds]
1889
2137
  );
1890
2138
  const optimizationOptions = useMemo2(
1891
- () => OPTIMIZATION_OPTION_GROUPS.flatMap(
2139
+ () => effectiveSoftConstraintGroups.flatMap(
1892
2140
  (group) => group.options.map((option) => ({
1893
2141
  ...option,
1894
2142
  groupId: group.id,
1895
2143
  groupName: group.name
1896
2144
  }))
1897
2145
  ),
1898
- []
2146
+ [effectiveSoftConstraintGroups]
1899
2147
  );
1900
2148
  const selectedOptimizationOptions = useMemo2(
2149
+ () => optimizationOptions.filter((option) => selectedOptimizationIds.includes(option.id)),
2150
+ [optimizationOptions, selectedOptimizationIds]
2151
+ );
2152
+ const selectedOptimizationDisplayOptions = useMemo2(
1901
2153
  () => optimizationOptions.filter(
1902
- (option) => selectedOptimizationIds.includes(option.id)
2154
+ (option) => selectedOptimizationIds.includes(option.id) || option.readOnly && option.defaultSelected
1903
2155
  ),
1904
2156
  [optimizationOptions, selectedOptimizationIds]
1905
2157
  );
2158
+ const effectiveSelectedConstraintIds = useMemo2(
2159
+ () => Array.from(
2160
+ /* @__PURE__ */ new Set([
2161
+ ...selectedConstraintIds,
2162
+ ...effectiveHardConstraintOptions.filter((option) => option.readOnly && option.defaultSelected).map((option) => option.id)
2163
+ ])
2164
+ ),
2165
+ [selectedConstraintIds, effectiveHardConstraintOptions]
2166
+ );
2167
+ const effectiveSelectedOptimizationIds = useMemo2(
2168
+ () => Array.from(
2169
+ /* @__PURE__ */ new Set([
2170
+ ...selectedOptimizationIds,
2171
+ ...optimizationOptions.filter((option) => option.readOnly && option.defaultSelected).map((option) => option.id)
2172
+ ])
2173
+ ),
2174
+ [selectedOptimizationIds, optimizationOptions]
2175
+ );
1906
2176
  const hasEmptyInputs = useMemo2(() => {
1907
- const hasEmptyConstraint = selectedConstraints.some(
1908
- (opt) => {
1909
- var _a;
1910
- return !((_a = constraintValues[opt.id]) == null ? void 0 : _a.trim());
1911
- }
1912
- );
1913
- const hasEmptyWeight = selectedOptimizationOptions.length >= 2 && selectedOptimizationOptions.some(
1914
- (opt) => {
1915
- var _a;
1916
- return !((_a = optimizationValues[`${opt.id}_weightPoints`]) == null ? void 0 : _a.trim());
1917
- }
1918
- );
2177
+ const hasEmptyConstraint = selectedConstraints.filter((opt) => opt.defaultValue != null).some((opt) => {
2178
+ var _a;
2179
+ return !((_a = constraintValues[opt.id]) == null ? void 0 : _a.trim());
2180
+ });
2181
+ const hasEmptyWeight = selectedOptimizationOptions.length >= 2 && selectedOptimizationOptions.some((opt) => {
2182
+ var _a;
2183
+ return !((_a = optimizationValues[`${opt.id}_weightPoints`]) == null ? void 0 : _a.trim());
2184
+ });
1919
2185
  return hasEmptyConstraint || hasEmptyWeight;
1920
2186
  }, [selectedConstraints, constraintValues, selectedOptimizationOptions, optimizationValues]);
1921
2187
  const allocationSummary = useMemo2(() => {
@@ -1949,13 +2215,25 @@ function AutoSchedulerModal({
1949
2215
  setScreen("configure");
1950
2216
  setLastFailure(null);
1951
2217
  setSelectedLocationIds([]);
2218
+ setSelectedSubLocationIds([]);
2219
+ setSelectedTeamIds([]);
1952
2220
  setSelectedOptimizationIds([]);
1953
2221
  setSelectedConstraintIds([]);
1954
2222
  setConstraintValues({});
1955
2223
  setOptimizationValues({});
2224
+ setSelectedScheduleDates([]);
1956
2225
  resetProgress();
1957
2226
  }
1958
2227
  }, [isOpen]);
2228
+ useEffect(() => {
2229
+ if (!isOpen || !scheduleWeekStartYmd) return;
2230
+ if (weekDatesOrdered.length === 0) {
2231
+ setSelectedScheduleDates([]);
2232
+ return;
2233
+ }
2234
+ const today = effectiveTodayCalendarYmd != null ? effectiveTodayCalendarYmd : weekDatesOrdered[0];
2235
+ setSelectedScheduleDates(defaultSelectedScheduleDates(weekDatesOrdered, today));
2236
+ }, [isOpen, scheduleWeekStartYmd, weekDatesOrdered, effectiveTodayCalendarYmd]);
1959
2237
  useEffect(() => {
1960
2238
  if (isOpen && initialConfig) {
1961
2239
  setSelectedLocationIds(initialConfig.selectedLocationIds);
@@ -1987,6 +2265,13 @@ function AutoSchedulerModal({
1987
2265
  }
1988
2266
  }, [selectedLocationIds, initialConfig, getStoredConfigForLocations]);
1989
2267
  useEffect(() => () => clearTimers(), []);
2268
+ useEffect(() => {
2269
+ if (onGenerate) return;
2270
+ setSelectedLocationIds((previous) => {
2271
+ const filtered = previous.filter((id) => locationIdsWithOpenShifts.has(id));
2272
+ return filtered.length === previous.length ? previous : filtered;
2273
+ });
2274
+ }, [locationIdsWithOpenShifts, onGenerate]);
1990
2275
  const stepStatusForIndex = (index) => {
1991
2276
  if (index <= completedStepIndex) {
1992
2277
  return "done";
@@ -2014,144 +2299,249 @@ function AutoSchedulerModal({
2014
2299
  timersRef.current.push(timerId);
2015
2300
  };
2016
2301
  const afterSuccessSteps = (runId, response) => {
2017
- schedule(STEP_INTERVAL_MS, () => {
2018
- if (runIdRef.current !== runId) return;
2019
- setCompletedStepIndex(2);
2020
- setActiveStepIndex(3);
2021
- }, runId);
2022
- schedule(STEP_INTERVAL_MS * 2, () => {
2023
- if (runIdRef.current !== runId) return;
2024
- setCompletedStepIndex(3);
2025
- setActiveStepIndex(4);
2026
- }, runId);
2027
- schedule(STEP_INTERVAL_MS * 3, async () => {
2028
- if (runIdRef.current !== runId) return;
2029
- try {
2030
- await onSolution(response.solution);
2031
- setCompletedStepIndex(4);
2302
+ schedule(
2303
+ STEP_INTERVAL_MS,
2304
+ () => {
2305
+ if (runIdRef.current !== runId) return;
2306
+ setCompletedStepIndex(2);
2307
+ setActiveStepIndex(3);
2308
+ },
2309
+ runId
2310
+ );
2311
+ schedule(
2312
+ STEP_INTERVAL_MS * 2,
2313
+ () => {
2314
+ if (runIdRef.current !== runId) return;
2315
+ setCompletedStepIndex(3);
2032
2316
  setActiveStepIndex(4);
2033
- } catch (err) {
2034
- console.error(err);
2035
- const defaultErrorMessage = "Failed to create schedule shifts";
2036
- const detailedErrorMessage = err instanceof Error ? err.message || defaultErrorMessage : defaultErrorMessage;
2037
- setLastFailure({
2038
- violatedConstraints: [
2039
- {
2040
- title: "Something went wrong",
2041
- detail: detailedErrorMessage
2042
- }
2043
- ],
2044
- recommendedFixes: []
2045
- });
2046
- setScreen("failed");
2047
- }
2048
- }, runId);
2317
+ },
2318
+ runId
2319
+ );
2320
+ schedule(
2321
+ STEP_INTERVAL_MS * 3,
2322
+ async () => {
2323
+ if (runIdRef.current !== runId) return;
2324
+ try {
2325
+ await (onSolution == null ? void 0 : onSolution(response.solution));
2326
+ setCompletedStepIndex(4);
2327
+ setActiveStepIndex(4);
2328
+ } catch (err) {
2329
+ console.error(err);
2330
+ const defaultErrorMessage = "Failed to create schedule shifts";
2331
+ const detailedErrorMessage = err instanceof Error ? err.message || defaultErrorMessage : defaultErrorMessage;
2332
+ setLastFailure({
2333
+ violatedConstraints: [
2334
+ {
2335
+ title: "Something went wrong",
2336
+ detail: detailedErrorMessage
2337
+ }
2338
+ ],
2339
+ recommendedFixes: []
2340
+ });
2341
+ setScreen("failed");
2342
+ }
2343
+ },
2344
+ runId
2345
+ );
2049
2346
  };
2050
2347
  const handleCreateSchedule = () => {
2051
2348
  if (selectedCount === 0) {
2052
2349
  return;
2053
2350
  }
2351
+ if (scheduleWeekStartYmd && selectedScheduleDates.length === 0) {
2352
+ return;
2353
+ }
2354
+ if (onGenerate) {
2355
+ const runId2 = resetBeforeSubmit();
2356
+ schedule(
2357
+ STEP_INTERVAL_MS,
2358
+ async () => {
2359
+ if (runIdRef.current !== runId2) return;
2360
+ setCompletedStepIndex(0);
2361
+ setActiveStepIndex(1);
2362
+ try {
2363
+ const generateResult = await onGenerate({
2364
+ selectedLocationIds,
2365
+ selectedSubLocationIds,
2366
+ selectedTeamIds,
2367
+ schedulerTimezone: selectedLocationTimezone,
2368
+ selectedScheduleDates,
2369
+ selectedConstraintIds: effectiveSelectedConstraintIds,
2370
+ constraintValues,
2371
+ selectedOptimizationIds: effectiveSelectedOptimizationIds,
2372
+ optimizationValues,
2373
+ selectedBuiltinIds: defaultSelectedBuiltinIds
2374
+ });
2375
+ if (runIdRef.current !== runId2) return;
2376
+ setCompletedStepIndex(1);
2377
+ setActiveStepIndex(2);
2378
+ schedule(
2379
+ STEP_INTERVAL_MS,
2380
+ () => {
2381
+ if (runIdRef.current !== runId2) return;
2382
+ setCompletedStepIndex(2);
2383
+ setActiveStepIndex(3);
2384
+ },
2385
+ runId2
2386
+ );
2387
+ schedule(
2388
+ STEP_INTERVAL_MS * 2,
2389
+ () => {
2390
+ if (runIdRef.current !== runId2) return;
2391
+ setCompletedStepIndex(3);
2392
+ setActiveStepIndex(4);
2393
+ },
2394
+ runId2
2395
+ );
2396
+ schedule(
2397
+ STEP_INTERVAL_MS * 3,
2398
+ () => {
2399
+ if (runIdRef.current !== runId2) return;
2400
+ setCompletedStepIndex(4);
2401
+ setActiveStepIndex(4);
2402
+ onGenerateSolution == null ? void 0 : onGenerateSolution(generateResult);
2403
+ onClose();
2404
+ },
2405
+ runId2
2406
+ );
2407
+ } catch (err) {
2408
+ if (runIdRef.current !== runId2) return;
2409
+ if (err instanceof InfeasibleScheduleError) {
2410
+ setLastFailure(err.failure);
2411
+ } else {
2412
+ const msg = err instanceof Error ? err.message : "Unknown error";
2413
+ setLastFailure({
2414
+ violatedConstraints: [{ title: "Error", detail: msg }],
2415
+ recommendedFixes: []
2416
+ });
2417
+ }
2418
+ setScreen("failed");
2419
+ }
2420
+ },
2421
+ runId2
2422
+ );
2423
+ return;
2424
+ }
2054
2425
  const payload = buildSchedulePayload({
2055
2426
  employees,
2056
2427
  openShifts,
2057
2428
  timeOffs,
2058
2429
  selectedLocationIds,
2059
2430
  jurisdictions,
2060
- selectedConstraintIds,
2431
+ selectedConstraintIds: effectiveSelectedConstraintIds,
2061
2432
  constraintValues,
2062
- selectedOptimizationIds,
2063
- optimizationValues
2433
+ selectedOptimizationIds: effectiveSelectedOptimizationIds,
2434
+ optimizationValues,
2435
+ trafficForecast,
2436
+ constraintDefinitionIds,
2437
+ constraintDefinitions,
2438
+ organizationId
2064
2439
  });
2065
2440
  persistConfigForLocations == null ? void 0 : persistConfigForLocations(selectedLocationIds, {
2066
2441
  selectedLocationIds,
2067
- selectedConstraintIds,
2442
+ selectedConstraintIds: effectiveSelectedConstraintIds,
2068
2443
  constraintValues,
2069
- selectedOptimizationIds,
2070
- optimizationValues
2444
+ selectedOptimizationIds: effectiveSelectedOptimizationIds,
2445
+ optimizationValues,
2446
+ selectedBuiltinIds: defaultSelectedBuiltinIds
2071
2447
  });
2072
2448
  const runId = resetBeforeSubmit();
2073
- schedule(STEP_INTERVAL_MS, async () => {
2074
- setCompletedStepIndex(0);
2075
- setActiveStepIndex(1);
2076
- try {
2077
- const token = await getToken(getTokenURLAndHeaders.url, getTokenURLAndHeaders.headers);
2078
- if (!token) {
2079
- throw new Error("Failed to get auto-scheduler token");
2080
- }
2081
- const auth = { authorization: `Bearer ${token}` };
2082
- const { jobId } = await submitSolveJob(baseURL, payload, auth);
2083
- if (runIdRef.current !== runId) {
2084
- return;
2085
- }
2086
- setCompletedStepIndex(1);
2087
- setActiveStepIndex(OPTIMIZING_STEP_INDEX);
2088
- const pollForResult = async () => {
2089
- if (runIdRef.current !== runId) return;
2090
- try {
2091
- const statusRes = await getJobStatus(baseURL, jobId, auth);
2449
+ schedule(
2450
+ STEP_INTERVAL_MS,
2451
+ async () => {
2452
+ setCompletedStepIndex(0);
2453
+ setActiveStepIndex(1);
2454
+ try {
2455
+ const token = await getToken(getTokenURLAndHeaders.url, getTokenURLAndHeaders.headers);
2456
+ if (!token) {
2457
+ throw new Error("Failed to get auto-scheduler token");
2458
+ }
2459
+ const selectedDates = [...selectedScheduleDates].sort();
2460
+ const horizonOptions = selectedDates.length > 0 ? {
2461
+ horizonStartDate: selectedDates[0],
2462
+ horizonEndDate: selectedDates[selectedDates.length - 1]
2463
+ } : {};
2464
+ const auth = { authorization: `Bearer ${token}`, ...horizonOptions };
2465
+ const { jobId } = await submitSolveJob(baseURL, payload, auth);
2466
+ if (runIdRef.current !== runId) {
2467
+ return;
2468
+ }
2469
+ setCompletedStepIndex(1);
2470
+ setActiveStepIndex(OPTIMIZING_STEP_INDEX);
2471
+ const pollForResult = async () => {
2092
2472
  if (runIdRef.current !== runId) return;
2093
- if (statusRes.status === "NOT_SOLVING" || statusRes.status === "TERMINATED_EARLY") {
2473
+ try {
2474
+ const statusRes = await getJobStatus(baseURL, jobId, auth);
2475
+ if (runIdRef.current !== runId) return;
2476
+ if (statusRes.status === "NOT_SOLVING" || statusRes.status === "TERMINATED_EARLY") {
2477
+ if (pollIntervalRef.current) {
2478
+ clearInterval(pollIntervalRef.current);
2479
+ pollIntervalRef.current = null;
2480
+ }
2481
+ const response = await getJobResult(baseURL, jobId, auth);
2482
+ if (runIdRef.current !== runId) return;
2483
+ let responseForHandling = response;
2484
+ if (response.explanation.isFeasible) {
2485
+ const solution = await getJobSolution(baseURL, jobId, auth);
2486
+ if (runIdRef.current !== runId) return;
2487
+ responseForHandling = { ...response, solution };
2488
+ }
2489
+ handleSolveResult(runId, responseForHandling);
2490
+ return;
2491
+ }
2492
+ } catch (err) {
2493
+ if (runIdRef.current !== runId) return;
2094
2494
  if (pollIntervalRef.current) {
2095
2495
  clearInterval(pollIntervalRef.current);
2096
2496
  pollIntervalRef.current = null;
2097
2497
  }
2098
- const response = await getJobResult(baseURL, jobId, auth);
2099
- if (runIdRef.current !== runId) return;
2100
- let responseForHandling = response;
2101
- if (response.explanation.isFeasible) {
2102
- const solution = await getJobSolution(baseURL, jobId, auth);
2103
- if (runIdRef.current !== runId) return;
2104
- responseForHandling = { ...response, solution };
2105
- }
2106
- handleSolveResult(runId, responseForHandling);
2107
- return;
2108
- }
2109
- } catch (err) {
2110
- if (runIdRef.current !== runId) return;
2111
- if (pollIntervalRef.current) {
2112
- clearInterval(pollIntervalRef.current);
2113
- pollIntervalRef.current = null;
2498
+ console.error(err);
2499
+ setLastFailure(NETWORK_ERROR_FAILURE);
2500
+ setScreen("failed");
2114
2501
  }
2115
- console.error(err);
2116
- setLastFailure(NETWORK_ERROR_FAILURE);
2117
- setScreen("failed");
2502
+ };
2503
+ pollForResult();
2504
+ pollIntervalRef.current = setInterval(pollForResult, POLL_INTERVAL_MS);
2505
+ } catch (err) {
2506
+ if (runIdRef.current !== runId) {
2507
+ return;
2118
2508
  }
2119
- };
2120
- pollForResult();
2121
- pollIntervalRef.current = setInterval(pollForResult, POLL_INTERVAL_MS);
2122
- } catch (err) {
2123
- if (runIdRef.current !== runId) {
2124
- return;
2509
+ console.error(err);
2510
+ setLastFailure(NETWORK_ERROR_FAILURE);
2511
+ setScreen("failed");
2125
2512
  }
2126
- console.error(err);
2127
- setLastFailure(NETWORK_ERROR_FAILURE);
2128
- setScreen("failed");
2129
- }
2130
- }, runId);
2513
+ },
2514
+ runId
2515
+ );
2131
2516
  };
2132
2517
  const handleSolveResult = (runId, response) => {
2133
- var _a, _b;
2518
+ var _a;
2134
2519
  if (!response.explanation.isFeasible) {
2135
- const result = mapViolationsToResult(
2136
- (_a = response.explanation.violations) != null ? _a : [],
2137
- response.explanation.summary
2138
- );
2139
- const selectedLocations = jurisdictions.flatMap((j) => j.locations).filter(
2140
- (l) => selectedLocationIds.includes(l.id)
2141
- );
2520
+ const result = mapViolationsToResult((_a = response.explanation.violations) != null ? _a : [], response.explanation.summary, dynamicViolationMessages);
2521
+ const selectedLocations = jurisdictions.flatMap((j) => j.locations).filter((l) => selectedLocationIds.includes(l.id));
2142
2522
  const recommendationPayload = generateRecommendationsURLAndHeaders ? {
2143
2523
  context: {
2144
2524
  selectedLocationIds,
2145
- selectedConstraintIds,
2525
+ selectedConstraintIds: effectiveSelectedConstraintIds,
2146
2526
  constraintValues,
2147
- selectedOptimizationIds,
2527
+ selectedOptimizationIds: effectiveSelectedOptimizationIds,
2148
2528
  optimizationValues,
2149
- timezone: (_b = selectedLocations[0]) == null ? void 0 : _b.timezone
2529
+ timezone: selectedLocationTimezone
2150
2530
  },
2151
2531
  locations: selectedLocations,
2152
2532
  employees,
2153
- openShifts: openShifts.filter((s) => !s.employeeId),
2154
- assignedShifts: openShifts.filter((s) => Boolean(s.employeeId)),
2533
+ openShifts: openShifts.filter(
2534
+ (s) => {
2535
+ var _a2;
2536
+ return !s.employeeId && selectedLocationIds.includes(String((_a2 = s.location) == null ? void 0 : _a2.id));
2537
+ }
2538
+ ),
2539
+ assignedShifts: openShifts.filter(
2540
+ (s) => {
2541
+ var _a2;
2542
+ return Boolean(s.employeeId) && selectedLocationIds.includes(String((_a2 = s.location) == null ? void 0 : _a2.id));
2543
+ }
2544
+ ),
2155
2545
  timeOffs: timeOffs != null ? timeOffs : [],
2156
2546
  explanation: response.explanation
2157
2547
  } : void 0;
@@ -2170,14 +2560,34 @@ function AutoSchedulerModal({
2170
2560
  setScreen("configure");
2171
2561
  };
2172
2562
  const handleToggleLocation = (locationId) => {
2173
- if (selectedLocationIds.includes(locationId)) {
2174
- setSelectedLocationIds(selectedLocationIds.filter((id) => id !== locationId));
2563
+ if (!onGenerate && !locationIdsWithOpenShifts.has(locationId)) {
2175
2564
  return;
2176
2565
  }
2177
- setSelectedLocationIds([...selectedLocationIds, locationId]);
2566
+ const newLocationIds = selectedLocationIds.includes(locationId) ? selectedLocationIds.filter((id) => id !== locationId) : [...selectedLocationIds, locationId];
2567
+ setSelectedLocationIds(newLocationIds);
2568
+ const filteredSubs = selectedSubLocationIds.filter((subId) => {
2569
+ const sub = subLocations == null ? void 0 : subLocations.find((s) => s.id === subId);
2570
+ return sub ? newLocationIds.includes(sub.parentId) : false;
2571
+ });
2572
+ if (filteredSubs.length !== selectedSubLocationIds.length) {
2573
+ setSelectedSubLocationIds(filteredSubs);
2574
+ }
2575
+ onSelectionChange == null ? void 0 : onSelectionChange(
2576
+ newLocationIds,
2577
+ filteredSubs.length !== selectedSubLocationIds.length ? filteredSubs : selectedSubLocationIds
2578
+ );
2579
+ };
2580
+ const handleToggleSubLocation = (subLocationId) => {
2581
+ const next = selectedSubLocationIds.includes(subLocationId) ? selectedSubLocationIds.filter((id) => id !== subLocationId) : [...selectedSubLocationIds, subLocationId];
2582
+ setSelectedSubLocationIds(next);
2583
+ onSelectionChange == null ? void 0 : onSelectionChange(selectedLocationIds, next);
2584
+ };
2585
+ const handleToggleTeam = (teamId) => {
2586
+ setSelectedTeamIds((prev) => prev.includes(teamId) ? prev.filter((id) => id !== teamId) : [...prev, teamId]);
2178
2587
  };
2179
2588
  const handleToggleConstraint = (constraintId) => {
2180
2589
  setSelectedConstraintIds((prev) => {
2590
+ var _a;
2181
2591
  if (prev.includes(constraintId)) {
2182
2592
  setConstraintValues((values) => {
2183
2593
  const next = { ...values };
@@ -2186,10 +2596,11 @@ function AutoSchedulerModal({
2186
2596
  });
2187
2597
  return prev.filter((id) => id !== constraintId);
2188
2598
  }
2189
- if (HARD_CONSTRAINT_DEFAULTS[constraintId]) {
2599
+ const defaultVal = (_a = effectiveHardConstraintOptions.find((o) => o.id === constraintId)) == null ? void 0 : _a.defaultValue;
2600
+ if (defaultVal) {
2190
2601
  setConstraintValues((values) => ({
2191
2602
  ...values,
2192
- [constraintId]: HARD_CONSTRAINT_DEFAULTS[constraintId]
2603
+ [constraintId]: defaultVal
2193
2604
  }));
2194
2605
  }
2195
2606
  return [...prev, constraintId];
@@ -2233,6 +2644,7 @@ function AutoSchedulerModal({
2233
2644
  violatedConstraints: lastFailure,
2234
2645
  title: violationTitle,
2235
2646
  subtitle: "",
2647
+ errorToast,
2236
2648
  generateRecommendationsURLAndHeaders,
2237
2649
  styles
2238
2650
  }
@@ -2240,12 +2652,12 @@ function AutoSchedulerModal({
2240
2652
  const configurationContent = /* @__PURE__ */ jsxs3(Stack2, { spacing: "20px", children: [
2241
2653
  /* @__PURE__ */ jsxs3(Box2, { sx: styles.banner, children: [
2242
2654
  /* @__PURE__ */ jsx3(Box2, { sx: styles.bannerIcon, children: /* @__PURE__ */ jsx3(MagicIcon, {}) }),
2243
- bannerText ? /* @__PURE__ */ jsx3(Text2, { sx: styles.bannerText, children: bannerText }) : /* @__PURE__ */ jsxs3(Stack2, { spacing: "4px", flex: 1, children: [
2244
- /* @__PURE__ */ jsx3(Text2, { sx: styles.bannerTitle, children: "Create a smarter retail schedule in seconds." }),
2245
- /* @__PURE__ */ jsx3(Text2, { sx: styles.bannerText, children: "AI assigns open shifts using your rules, availability, and optimization priorities while keeping existing assigned shifts in place." })
2655
+ /* @__PURE__ */ jsxs3(Stack2, { spacing: "4px", flex: 1, children: [
2656
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.bannerTitle, children: bannerTitle }),
2657
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.bannerText, children: bannerText })
2246
2658
  ] })
2247
2659
  ] }),
2248
- openShifts.length === 0 ? /* @__PURE__ */ jsxs3(
2660
+ !onGenerate && openShifts.length === 0 ? /* @__PURE__ */ jsxs3(
2249
2661
  Box2,
2250
2662
  {
2251
2663
  sx: {
@@ -2288,25 +2700,33 @@ function AutoSchedulerModal({
2288
2700
  ] }) }) }),
2289
2701
  /* @__PURE__ */ jsx3(PopoverContent, { sx: styles.popoverContent, children: /* @__PURE__ */ jsx3(PopoverBody, { p: 0, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "4px", children: jurisdictions.map((jurisdiction) => /* @__PURE__ */ jsxs3(Box2, { children: [
2290
2702
  /* @__PURE__ */ jsx3(Text2, { sx: styles.groupHeader, children: jurisdiction.title }),
2291
- /* @__PURE__ */ jsx3(Stack2, { spacing: "2px", children: jurisdiction.locations.map((location) => /* @__PURE__ */ jsxs3(
2292
- HStack2,
2293
- {
2294
- as: "label",
2295
- sx: { ...styles.optionRow, cursor: "pointer" },
2296
- children: [
2297
- /* @__PURE__ */ jsx3(
2298
- Checkbox,
2299
- {
2300
- id: `location-${location.id}`,
2301
- isChecked: selectedLocationIds.includes(location.id),
2302
- onChange: () => handleToggleLocation(location.id)
2303
- }
2304
- ),
2305
- /* @__PURE__ */ jsx3(Text2, { sx: styles.optionLabel, children: location.name })
2306
- ]
2307
- },
2308
- location.id
2309
- )) })
2703
+ /* @__PURE__ */ jsx3(Stack2, { spacing: "2px", children: jurisdiction.locations.map((location) => {
2704
+ const isEnabled = onGenerate || locationIdsWithOpenShifts.has(location.id);
2705
+ return /* @__PURE__ */ jsxs3(
2706
+ HStack2,
2707
+ {
2708
+ as: "label",
2709
+ sx: {
2710
+ ...styles.optionRow,
2711
+ cursor: isEnabled ? "pointer" : "not-allowed",
2712
+ opacity: isEnabled ? 1 : 0.55
2713
+ },
2714
+ children: [
2715
+ /* @__PURE__ */ jsx3(
2716
+ Checkbox,
2717
+ {
2718
+ id: `location-${location.id}`,
2719
+ isChecked: selectedLocationIds.includes(location.id),
2720
+ isDisabled: !isEnabled,
2721
+ onChange: () => handleToggleLocation(location.id)
2722
+ }
2723
+ ),
2724
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.optionLabel, children: location.name })
2725
+ ]
2726
+ },
2727
+ location.id
2728
+ );
2729
+ }) })
2310
2730
  ] }, jurisdiction.id)) }) }) })
2311
2731
  ] }),
2312
2732
  /* @__PURE__ */ jsx3(Stack2, { spacing: "8px", children: selectedByJurisdiction.map((jurisdiction) => /* @__PURE__ */ jsxs3(Box2, { sx: styles.selectedBanner, children: [
@@ -2320,11 +2740,128 @@ function AutoSchedulerModal({
2320
2740
  ] })
2321
2741
  ] }, jurisdiction.id)) })
2322
2742
  ] }),
2743
+ showSubLocations && /* @__PURE__ */ jsxs3(Stack2, { spacing: "8px", sx: styles.locationSection, children: [
2744
+ /* @__PURE__ */ jsx3(Stack2, { spacing: "4px", children: /* @__PURE__ */ jsxs3(HStack2, { sx: styles.locationLabelRow, children: [
2745
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.locationLabel, children: "Sub-location" }),
2746
+ selectedSubLocationIds.length > 0 && /* @__PURE__ */ jsx3(Badge, { sx: styles.locationBadge, children: selectedSubLocationIds.length })
2747
+ ] }) }),
2748
+ /* @__PURE__ */ jsxs3(Popover, { placement: "bottom-start", matchWidth: true, isLazy: true, children: [
2749
+ /* @__PURE__ */ jsx3(PopoverTrigger, { children: /* @__PURE__ */ jsx3(
2750
+ Button2,
2751
+ {
2752
+ variant: "unstyled",
2753
+ isDisabled: selectedLocationIds.length === 0 || filteredSubLocations.length === 0,
2754
+ sx: {
2755
+ ...styles.locationTrigger,
2756
+ ...selectedLocationIds.length === 0 || filteredSubLocations.length === 0 ? { opacity: 0.5, cursor: "not-allowed" } : {}
2757
+ },
2758
+ children: /* @__PURE__ */ jsxs3(HStack2, { justify: "space-between", w: "full", children: [
2759
+ /* @__PURE__ */ jsx3(
2760
+ Text2,
2761
+ {
2762
+ sx: {
2763
+ ...styles.locationTriggerText,
2764
+ ...selectedSubLocationIds.length > 0 ? { color: "#303030" } : {}
2765
+ },
2766
+ noOfLines: 1,
2767
+ children: selectedSubLocationIds.length > 0 ? filteredSubLocations.filter((s) => selectedSubLocationIds.includes(s.id)).map((s) => s.name).join(", ") : "Select sub-locations"
2768
+ }
2769
+ ),
2770
+ /* @__PURE__ */ jsx3(Box2, { color: "#5c5c5c", children: /* @__PURE__ */ jsx3(ChevronDownIcon, {}) })
2771
+ ] })
2772
+ }
2773
+ ) }),
2774
+ /* @__PURE__ */ jsx3(PopoverContent, { sx: styles.popoverContent, children: /* @__PURE__ */ jsx3(PopoverBody, { p: 0, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "2px", children: filteredSubLocations.map((sub) => /* @__PURE__ */ jsxs3(HStack2, { as: "label", sx: { ...styles.optionRow, cursor: "pointer" }, children: [
2775
+ /* @__PURE__ */ jsx3(
2776
+ Checkbox,
2777
+ {
2778
+ id: `sublocation-${sub.id}`,
2779
+ isChecked: selectedSubLocationIds.includes(sub.id),
2780
+ onChange: () => handleToggleSubLocation(sub.id)
2781
+ }
2782
+ ),
2783
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.optionLabel, children: sub.name })
2784
+ ] }, sub.id)) }) }) })
2785
+ ] })
2786
+ ] }),
2787
+ showTeams && /* @__PURE__ */ jsxs3(Stack2, { spacing: "8px", sx: styles.locationSection, children: [
2788
+ /* @__PURE__ */ jsx3(Stack2, { spacing: "4px", children: /* @__PURE__ */ jsxs3(HStack2, { sx: styles.locationLabelRow, children: [
2789
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.locationLabel, children: "Teams" }),
2790
+ selectedTeamIds.length > 0 && /* @__PURE__ */ jsx3(Badge, { sx: styles.locationBadge, children: selectedTeamIds.length })
2791
+ ] }) }),
2792
+ /* @__PURE__ */ jsxs3(Popover, { placement: "bottom-start", matchWidth: true, isLazy: true, children: [
2793
+ /* @__PURE__ */ jsx3(PopoverTrigger, { children: /* @__PURE__ */ jsx3(
2794
+ Button2,
2795
+ {
2796
+ variant: "unstyled",
2797
+ isDisabled: !teams || teams.length === 0,
2798
+ sx: {
2799
+ ...styles.locationTrigger,
2800
+ ...!teams || teams.length === 0 ? { opacity: 0.5, cursor: "not-allowed" } : {}
2801
+ },
2802
+ children: /* @__PURE__ */ jsxs3(HStack2, { justify: "space-between", w: "full", children: [
2803
+ /* @__PURE__ */ jsx3(
2804
+ Text2,
2805
+ {
2806
+ sx: {
2807
+ ...styles.locationTriggerText,
2808
+ ...selectedTeamIds.length > 0 ? { color: "#303030" } : {}
2809
+ },
2810
+ noOfLines: 1,
2811
+ children: selectedTeamIds.length > 0 ? (teams != null ? teams : []).filter((t) => selectedTeamIds.includes(t.id)).map((t) => t.name).join(", ") : "Select teams"
2812
+ }
2813
+ ),
2814
+ /* @__PURE__ */ jsx3(Box2, { color: "#5c5c5c", children: /* @__PURE__ */ jsx3(ChevronDownIcon, {}) })
2815
+ ] })
2816
+ }
2817
+ ) }),
2818
+ /* @__PURE__ */ jsx3(PopoverContent, { sx: styles.popoverContent, children: /* @__PURE__ */ jsx3(PopoverBody, { p: 0, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "2px", children: (teams != null ? teams : []).map((team) => /* @__PURE__ */ jsxs3(HStack2, { as: "label", sx: { ...styles.optionRow, cursor: "pointer" }, children: [
2819
+ /* @__PURE__ */ jsx3(
2820
+ Checkbox,
2821
+ {
2822
+ id: `team-${team.id}`,
2823
+ isChecked: selectedTeamIds.includes(team.id),
2824
+ onChange: () => handleToggleTeam(team.id)
2825
+ }
2826
+ ),
2827
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.optionLabel, children: team.name })
2828
+ ] }, team.id)) }) }) })
2829
+ ] })
2830
+ ] }),
2831
+ scheduleWeekStartYmd ? /* @__PURE__ */ jsxs3(Stack2, { spacing: "8px", sx: styles.locationSection, children: [
2832
+ /* @__PURE__ */ jsx3(Text2, { sx: { ...styles.locationLabel, fontWeight: 600 }, children: "Generate schedule for:" }),
2833
+ /* @__PURE__ */ jsx3(HStack2, { spacing: 2, flexWrap: "wrap", children: weekDatesOrdered.map((ymd) => {
2834
+ const selected = selectedScheduleDates.includes(ymd);
2835
+ return /* @__PURE__ */ jsx3(
2836
+ Button2,
2837
+ {
2838
+ type: "button",
2839
+ variant: "unstyled",
2840
+ onClick: () => toggleScheduleDate(ymd),
2841
+ borderRadius: "full",
2842
+ minW: "40px",
2843
+ h: "40px",
2844
+ px: 0,
2845
+ fontSize: "sm",
2846
+ fontWeight: 600,
2847
+ borderWidth: selected ? "2px" : "1px",
2848
+ borderStyle: "solid",
2849
+ borderColor: selected ? "#5b44ff" : "#e2e8f0",
2850
+ bg: selected ? "#ede9fe" : "#ffffff",
2851
+ color: selected ? "#5b44ff" : "#303030",
2852
+ "aria-pressed": selected,
2853
+ "aria-label": `${weekdayTwoLetterFromYmd(ymd)} ${ymd}`,
2854
+ children: weekdayTwoLetterFromYmd(ymd)
2855
+ },
2856
+ ymd
2857
+ );
2858
+ }) })
2859
+ ] }) : null,
2323
2860
  /* @__PURE__ */ jsxs3(Stack2, { spacing: "16px", sx: styles.hardConstraintsSection, children: [
2324
2861
  /* @__PURE__ */ jsxs3(Stack2, { spacing: "4px", children: [
2325
2862
  /* @__PURE__ */ jsxs3(HStack2, { sx: styles.hardConstraintsLabelRow, children: [
2326
2863
  /* @__PURE__ */ jsx3(Text2, { sx: styles.hardConstraintsLabel, children: "Required rules" }),
2327
- selectedConstraintIds.length > 0 ? /* @__PURE__ */ jsx3(Badge, { sx: styles.hardConstraintsBadge, children: selectedConstraintIds.length }) : null
2864
+ selectedConstraintDisplayOptions.length > 0 ? /* @__PURE__ */ jsx3(Badge, { sx: styles.hardConstraintsBadge, children: selectedConstraintDisplayOptions.length }) : null
2328
2865
  ] }),
2329
2866
  /* @__PURE__ */ jsx3(Text2, { sx: styles.hardConstraintsSubtitle, children: "Assigned shifts will be required to meet the following rules:" })
2330
2867
  ] }),
@@ -2336,35 +2873,30 @@ function AutoSchedulerModal({
2336
2873
  {
2337
2874
  sx: {
2338
2875
  ...styles.hardConstraintsTriggerText,
2339
- ...selectedConstraintIds.length > 0 ? { color: "#303030" } : {}
2876
+ ...selectedConstraintDisplayOptions.length > 0 ? { color: "#303030" } : {}
2340
2877
  },
2341
2878
  noOfLines: 1,
2342
- children: selectedConstraintIds.length > 0 ? HARD_CONSTRAINT_OPTIONS.filter((o) => selectedConstraintIds.includes(o.id)).map((o) => o.name).join(", ") : "Select rules"
2879
+ children: selectedConstraintDisplayOptions.length > 0 ? selectedConstraintDisplayOptions.map((o) => o.name).join(", ") : "Select rules"
2343
2880
  }
2344
2881
  ),
2345
2882
  /* @__PURE__ */ jsx3(Box2, { color: "#5c5c5c", children: /* @__PURE__ */ jsx3(ChevronDownIcon, {}) })
2346
2883
  ] }) }) }),
2347
- /* @__PURE__ */ jsx3(PopoverContent, { sx: styles.hardConstraintsPopoverContent, children: /* @__PURE__ */ jsx3(PopoverBody, { p: 0, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "4px", children: HARD_CONSTRAINT_OPTIONS.map((option) => /* @__PURE__ */ jsxs3(
2348
- HStack2,
2349
- {
2350
- as: "label",
2351
- sx: { ...styles.hardConstraintsOptionRow, cursor: "pointer" },
2352
- children: [
2353
- /* @__PURE__ */ jsx3(
2354
- Checkbox,
2355
- {
2356
- id: `constraint-${option.id}`,
2357
- isChecked: selectedConstraintIds.includes(option.id),
2358
- onChange: () => handleToggleConstraint(option.id)
2359
- }
2360
- ),
2361
- /* @__PURE__ */ jsx3(Text2, { sx: styles.hardConstraintsOptionLabel, children: option.name })
2362
- ]
2363
- },
2364
- option.id
2365
- )) }) }) })
2884
+ /* @__PURE__ */ jsx3(PopoverContent, { sx: styles.hardConstraintsPopoverContent, children: /* @__PURE__ */ jsx3(PopoverBody, { p: 0, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "4px", children: effectiveHardConstraintOptions.map((option) => /* @__PURE__ */ jsxs3(HStack2, { as: "label", sx: { ...styles.hardConstraintsOptionRow, cursor: "pointer" }, children: [
2885
+ /* @__PURE__ */ jsx3(
2886
+ Checkbox,
2887
+ {
2888
+ id: `constraint-${option.id}`,
2889
+ isChecked: selectedConstraintIds.includes(option.id) || Boolean(option.readOnly && option.defaultSelected),
2890
+ isDisabled: option.readOnly,
2891
+ onChange: () => {
2892
+ if (!option.readOnly) handleToggleConstraint(option.id);
2893
+ }
2894
+ }
2895
+ ),
2896
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.hardConstraintsOptionLabel, children: option.name })
2897
+ ] }, option.id)) }) }) })
2366
2898
  ] }),
2367
- selectedConstraints.length > 0 ? /* @__PURE__ */ jsx3(Box2, { sx: styles.hardConstraintsInputsContainer, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "16px", children: selectedConstraints.map((option, index) => {
2899
+ selectedConstraints.filter((o) => o.defaultValue != null).length > 0 ? /* @__PURE__ */ jsx3(Box2, { sx: styles.hardConstraintsInputsContainer, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "16px", children: selectedConstraints.filter((o) => o.defaultValue != null).map((option, index) => {
2368
2900
  var _a;
2369
2901
  return /* @__PURE__ */ jsxs3(Stack2, { sx: styles.hardConstraintsInputItem, children: [
2370
2902
  /* @__PURE__ */ jsxs3(Text2, { sx: styles.hardConstraintsInputLabel, children: [
@@ -2382,13 +2914,13 @@ function AutoSchedulerModal({
2382
2914
  ...prev,
2383
2915
  [option.id]: event.target.value
2384
2916
  })),
2385
- min: option.id === "max_work_days_per_week" ? 1 : void 0,
2386
- max: option.id === "max_work_days_per_week" ? 6 : void 0,
2387
- step: option.id === "max_work_days_per_week" ? void 0 : "any",
2917
+ min: option.min,
2918
+ max: option.max,
2919
+ step: option.max != null ? void 0 : "any",
2388
2920
  sx: styles.hardConstraintsInputField
2389
2921
  }
2390
2922
  ),
2391
- /* @__PURE__ */ jsx3(InputRightAddon, { sx: styles.optimizationSuffix, children: option.id === "max_work_days_per_week" ? "days" : "hours" })
2923
+ /* @__PURE__ */ jsx3(InputRightAddon, { sx: styles.optimizationSuffix, children: option.suffix })
2392
2924
  ] })
2393
2925
  ] }, option.id);
2394
2926
  }) }) }) : null
@@ -2397,89 +2929,100 @@ function AutoSchedulerModal({
2397
2929
  /* @__PURE__ */ jsxs3(Stack2, { spacing: "4px", children: [
2398
2930
  /* @__PURE__ */ jsxs3(HStack2, { sx: styles.optimizationLabelRow, children: [
2399
2931
  /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationLabel, children: "Optimization" }),
2400
- selectedOptimizationIds.length > 0 ? /* @__PURE__ */ jsx3(Badge, { sx: styles.optimizationBadge, children: selectedOptimizationIds.length }) : null
2932
+ selectedOptimizationDisplayOptions.length > 0 ? /* @__PURE__ */ jsx3(Badge, { sx: styles.optimizationBadge, children: selectedOptimizationDisplayOptions.length }) : null
2401
2933
  ] }),
2402
2934
  /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationSubtitle, children: "Your schedule will be optimized based on the following priorities:" })
2403
2935
  ] }),
2404
- /* @__PURE__ */ jsxs3(Popover, { placement: "bottom-start", matchWidth: true, isLazy: true, children: [
2936
+ /* @__PURE__ */ jsxs3(Popover, { placement: "top-start", strategy: "fixed", matchWidth: true, isLazy: true, gutter: 4, children: [
2405
2937
  /* @__PURE__ */ jsx3(PopoverTrigger, { children: /* @__PURE__ */ jsx3(Button2, { variant: "unstyled", sx: styles.optimizationTrigger, children: /* @__PURE__ */ jsxs3(HStack2, { justify: "space-between", w: "full", children: [
2406
2938
  /* @__PURE__ */ jsx3(
2407
2939
  Text2,
2408
2940
  {
2409
2941
  sx: {
2410
2942
  ...styles.optimizationTriggerText,
2411
- ...selectedOptimizationIds.length > 0 ? { color: "#303030" } : {}
2943
+ ...selectedOptimizationDisplayOptions.length > 0 ? { color: "#303030" } : {}
2412
2944
  },
2413
2945
  noOfLines: 1,
2414
- children: selectedOptimizationIds.length > 0 ? optimizationOptions.filter((o) => selectedOptimizationIds.includes(o.id)).map((o) => o.title).join(", ") : "Select optimization priorities"
2946
+ children: selectedOptimizationDisplayOptions.length > 0 ? selectedOptimizationDisplayOptions.map((o) => o.title).join(", ") : "Select optimization priorities"
2415
2947
  }
2416
2948
  ),
2417
2949
  /* @__PURE__ */ jsx3(Box2, { color: "#5c5c5c", children: /* @__PURE__ */ jsx3(ChevronDownIcon, {}) })
2418
2950
  ] }) }) }),
2419
- /* @__PURE__ */ jsx3(PopoverContent, { sx: styles.optimizationPopoverContent, children: /* @__PURE__ */ jsx3(PopoverBody, { p: 0, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "4px", children: OPTIMIZATION_OPTION_GROUPS.map((group) => /* @__PURE__ */ jsxs3(Box2, { children: [
2420
- /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationGroupHeader, children: group.name }),
2421
- /* @__PURE__ */ jsx3(Stack2, { spacing: "2px", children: group.options.map((option) => /* @__PURE__ */ jsxs3(
2422
- Stack2,
2423
- {
2424
- as: "label",
2425
- spacing: "2px",
2426
- sx: { ...styles.optimizationOptionRow, cursor: "pointer" },
2427
- children: [
2428
- /* @__PURE__ */ jsxs3(HStack2, { spacing: "8px", children: [
2429
- /* @__PURE__ */ jsx3(
2430
- Checkbox,
2431
- {
2432
- id: `optimization-${option.id}`,
2433
- isChecked: selectedOptimizationIds.includes(option.id),
2434
- isDisabled: !selectedOptimizationIds.includes(option.id) && selectedOptimizationIds.length >= 3,
2435
- onChange: () => handleToggleOptimization(option.id)
2436
- }
2437
- ),
2438
- /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationOptionLabel, children: option.title })
2439
- ] }),
2440
- option.subtitle ? /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationOptionHelp, children: option.subtitle }) : null
2441
- ]
2951
+ /* @__PURE__ */ jsx3(Portal, { appendToParentPortal: false, children: /* @__PURE__ */ jsx3(
2952
+ PopoverContent,
2953
+ {
2954
+ sx: {
2955
+ ...styles.optimizationPopoverContent,
2956
+ maxH: "min(560px, calc(100vh - 96px))",
2957
+ overflowY: "auto",
2958
+ zIndex: 1500,
2959
+ scrollbarWidth: "none",
2960
+ msOverflowStyle: "none",
2961
+ "&::-webkit-scrollbar": { display: "none" }
2442
2962
  },
2443
- option.id
2444
- )) })
2445
- ] }, group.id)) }) }) })
2963
+ children: /* @__PURE__ */ jsx3(PopoverBody, { p: 0, children: /* @__PURE__ */ jsx3(Stack2, { spacing: "4px", children: effectiveSoftConstraintGroups.map((group) => /* @__PURE__ */ jsxs3(Box2, { children: [
2964
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationGroupHeader, children: group.name }),
2965
+ /* @__PURE__ */ jsx3(Stack2, { spacing: "2px", children: group.options.map((option) => /* @__PURE__ */ jsxs3(Box2, { children: [
2966
+ /* @__PURE__ */ jsxs3(
2967
+ Stack2,
2968
+ {
2969
+ as: "label",
2970
+ spacing: "2px",
2971
+ sx: { ...styles.optimizationOptionRow, cursor: "pointer" },
2972
+ children: [
2973
+ /* @__PURE__ */ jsxs3(HStack2, { spacing: "8px", children: [
2974
+ /* @__PURE__ */ jsx3(
2975
+ Checkbox,
2976
+ {
2977
+ id: `optimization-${option.id}`,
2978
+ isChecked: selectedOptimizationIds.includes(option.id) || Boolean(option.readOnly && option.defaultSelected),
2979
+ isDisabled: option.readOnly || !selectedOptimizationIds.includes(option.id) && selectedOptimizationIds.length >= 3,
2980
+ onChange: () => {
2981
+ if (!option.readOnly) handleToggleOptimization(option.id);
2982
+ }
2983
+ }
2984
+ ),
2985
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationOptionLabel, children: option.title })
2986
+ ] }),
2987
+ option.subtitle ? /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationOptionHelp, children: option.subtitle }) : null
2988
+ ]
2989
+ }
2990
+ ),
2991
+ optimizationFooter && (option.id === "cost_to_sales_ratio" || option.id === "maximize_sales") ? /* @__PURE__ */ jsx3(Box2, { px: "12px", pt: "4px", pb: "6px", children: resolveOptimizationFooter(optimizationFooter, option.id) }) : null
2992
+ ] }, option.id)) })
2993
+ ] }, group.id)) }) })
2994
+ }
2995
+ ) })
2446
2996
  ] }),
2447
2997
  selectedOptimizationOptions.length >= 2 ? /* @__PURE__ */ jsxs3(Box2, { sx: styles.optimizationInputsContainer, children: [
2448
2998
  /* @__PURE__ */ jsx3(Stack2, { spacing: "16px", children: selectedOptimizationOptions.map((option, index) => {
2449
2999
  var _a;
2450
- return /* @__PURE__ */ jsx3(
2451
- Stack2,
2452
- {
2453
- sx: styles.optimizationInputItem,
2454
- children: /* @__PURE__ */ jsxs3(Stack2, { sx: styles.hardConstraintsInputItem, children: [
2455
- /* @__PURE__ */ jsxs3(Text2, { sx: styles.hardConstraintsInputLabel, children: [
2456
- index + 1,
2457
- ". ",
2458
- option.title
2459
- ] }),
2460
- /* @__PURE__ */ jsxs3(InputGroup, { children: [
2461
- /* @__PURE__ */ jsx3(
2462
- Input,
2463
- {
2464
- type: "text",
2465
- value: (_a = optimizationValues[`${option.id}_weightPoints`]) != null ? _a : "1",
2466
- onChange: (e) => {
2467
- const val = e.target.value;
2468
- if (val !== "" && (!/^\d+$/.test(val) || Number(val) < 1)) return;
2469
- setOptimizationValues((prev) => ({
2470
- ...prev,
2471
- [`${option.id}_weightPoints`]: val
2472
- }));
2473
- },
2474
- sx: styles.hardConstraintsInputField
2475
- }
2476
- ),
2477
- /* @__PURE__ */ jsx3(InputRightAddon, { sx: styles.optimizationSuffix, children: "points" })
2478
- ] })
2479
- ] })
2480
- },
2481
- option.id
2482
- );
3000
+ return /* @__PURE__ */ jsx3(Stack2, { sx: styles.optimizationInputItem, children: /* @__PURE__ */ jsxs3(Stack2, { sx: styles.hardConstraintsInputItem, children: [
3001
+ /* @__PURE__ */ jsxs3(Text2, { sx: styles.hardConstraintsInputLabel, children: [
3002
+ index + 1,
3003
+ ". ",
3004
+ option.title
3005
+ ] }),
3006
+ /* @__PURE__ */ jsxs3(InputGroup, { children: [
3007
+ /* @__PURE__ */ jsx3(
3008
+ Input,
3009
+ {
3010
+ type: "text",
3011
+ value: (_a = optimizationValues[`${option.id}_weightPoints`]) != null ? _a : "1",
3012
+ onChange: (e) => {
3013
+ const val = e.target.value;
3014
+ if (val !== "" && (!/^\d+$/.test(val) || Number(val) < 1)) return;
3015
+ setOptimizationValues((prev) => ({
3016
+ ...prev,
3017
+ [`${option.id}_weightPoints`]: val
3018
+ }));
3019
+ },
3020
+ sx: styles.hardConstraintsInputField
3021
+ }
3022
+ ),
3023
+ /* @__PURE__ */ jsx3(InputRightAddon, { sx: styles.optimizationSuffix, children: "points" })
3024
+ ] })
3025
+ ] }) }, option.id);
2483
3026
  }) }),
2484
3027
  /* @__PURE__ */ jsx3(Divider, { sx: { ...styles.optimizationDivider, my: "16px" } }),
2485
3028
  /* @__PURE__ */ jsx3(Text2, { sx: styles.optimizationSummary, children: allocationSummary })
@@ -2488,7 +3031,9 @@ function AutoSchedulerModal({
2488
3031
  ] });
2489
3032
  const stepsContent = /* @__PURE__ */ jsxs3(Stack2, { spacing: "16px", sx: styles.stepsContainer, children: [
2490
3033
  showFailed ? violationPanel : null,
2491
- /* @__PURE__ */ jsx3(Stack2, { spacing: "14px", sx: styles.stepsList, children: SCHEDULE_STEPS.map((step, index) => {
3034
+ /* @__PURE__ */ jsx3(Stack2, { spacing: "14px", sx: styles.stepsList, children: SCHEDULE_STEPS.map((rawStep, index) => {
3035
+ const isQueueWait = index === 1 && solveStatus === "QUEUED";
3036
+ const step = isQueueWait ? { title: "Waiting in queue\u2026", subtitle: void 0 } : rawStep;
2492
3037
  const status = stepStatusForIndex(index);
2493
3038
  const isFailedStep = showFailed && index === activeStepIndex;
2494
3039
  const iconStyles = {
@@ -2506,26 +3051,26 @@ function AutoSchedulerModal({
2506
3051
  ...isFailedStep ? styles.stepTitleFailed : null
2507
3052
  };
2508
3053
  return /* @__PURE__ */ jsxs3(HStack2, { sx: styles.stepRow, children: [
2509
- /* @__PURE__ */ jsx3(Box2, { sx: iconStyles, children: isFailedStep ? /* @__PURE__ */ jsx3(FailedIcon, { width: 12, height: 12 }) : status === "active" ? /* @__PURE__ */ jsx3(Spinner2, { size: "sm", thickness: "2px", speed: "0.7s" }) : status === "done" ? /* @__PURE__ */ jsx3(CheckIcon, { width: 12, height: 12 }) : null }),
3054
+ /* @__PURE__ */ jsx3(Box2, { sx: iconStyles, children: isFailedStep ? /* @__PURE__ */ jsx3(FailedIcon, { width: 12, height: 12 }) : status === "active" ? /* @__PURE__ */ jsx3(Spinner2, { size: "sm", thickness: "2px", speed: "0.7s" }) : status === "done" ? /* @__PURE__ */ jsx3(CheckIcon, { width: 10, height: 10 }) : null }),
2510
3055
  /* @__PURE__ */ jsxs3(Box2, { children: [
2511
3056
  /* @__PURE__ */ jsx3(Text2, { sx: titleStyles, children: step.title }),
2512
3057
  status === "active" && step.subtitle ? /* @__PURE__ */ jsx3(Text2, { sx: styles.stepSubtitle, children: step.subtitle }) : null
2513
3058
  ] })
2514
- ] }, step.title);
3059
+ ] }, rawStep.title);
2515
3060
  }) })
2516
3061
  ] });
2517
- const modalFooter = showConfiguration ? /* @__PURE__ */ jsxs3(ModalFooter, { sx: styles.footer, children: [
3062
+ const footerButtons = showConfiguration ? /* @__PURE__ */ jsxs3(Fragment, { children: [
2518
3063
  /* @__PURE__ */ jsx3(Button2, { variant: "outline", onClick: onClose, sx: styles.cancelButton, children: cancelLabel }),
2519
3064
  /* @__PURE__ */ jsx3(
2520
3065
  Button2,
2521
3066
  {
2522
3067
  onClick: handleCreateSchedule,
2523
- isDisabled: selectedCount === 0 || openShifts.length === 0 || hasEmptyInputs,
3068
+ isDisabled: selectedCount === 0 || !onGenerate && openShifts.length === 0 || hasEmptyInputs || Boolean(scheduleWeekStartYmd) && selectedScheduleDates.length === 0,
2524
3069
  sx: styles.primaryButton,
2525
3070
  children: primaryActionLabel
2526
3071
  }
2527
3072
  )
2528
- ] }) : showFailed ? /* @__PURE__ */ jsxs3(ModalFooter, { sx: styles.failedFooter, children: [
3073
+ ] }) : showFailed ? /* @__PURE__ */ jsxs3(Fragment, { children: [
2529
3074
  /* @__PURE__ */ jsx3(
2530
3075
  Button2,
2531
3076
  {
@@ -2534,10 +3079,11 @@ function AutoSchedulerModal({
2534
3079
  if (lastFailure) {
2535
3080
  onFixManually == null ? void 0 : onFixManually(lastFailure, {
2536
3081
  selectedLocationIds,
2537
- selectedConstraintIds,
3082
+ selectedConstraintIds: effectiveSelectedConstraintIds,
2538
3083
  constraintValues,
2539
- selectedOptimizationIds,
2540
- optimizationValues
3084
+ selectedOptimizationIds: effectiveSelectedOptimizationIds,
3085
+ optimizationValues,
3086
+ selectedBuiltinIds: defaultSelectedBuiltinIds
2541
3087
  });
2542
3088
  }
2543
3089
  onClose();
@@ -2548,31 +3094,75 @@ function AutoSchedulerModal({
2548
3094
  ),
2549
3095
  /* @__PURE__ */ jsx3(Button2, { onClick: handleAdjustConstraints, sx: styles.failedPrimaryButton, children: "Adjust constraints" })
2550
3096
  ] }) : null;
2551
- const modalContent = /* @__PURE__ */ jsxs3(Modal, { isOpen, onClose, isCentered: true, motionPreset: "scale", closeOnOverlayClick: false, closeOnEsc: false, children: [
2552
- /* @__PURE__ */ jsx3(ModalOverlay, { sx: styles.overlay }),
2553
- /* @__PURE__ */ jsxs3(ModalContent, { sx: styles.content, children: [
2554
- /* @__PURE__ */ jsx3(ModalHeader, { sx: styles.header, children: /* @__PURE__ */ jsxs3(HStack2, { justify: "space-between", w: "full", children: [
2555
- /* @__PURE__ */ jsx3(Text2, { sx: styles.title, children: modalTitle }),
2556
- /* @__PURE__ */ jsx3(
2557
- IconButton2,
2558
- {
2559
- "aria-label": "Close modal",
2560
- variant: "ghost",
2561
- size: "sm",
2562
- icon: /* @__PURE__ */ jsx3(CloseIcon, {}),
2563
- onClick: onClose,
2564
- sx: styles.closeButton
2565
- }
2566
- )
2567
- ] }) }),
2568
- /* @__PURE__ */ jsx3(ModalBody, { sx: showConfiguration ? styles.body : { ...styles.body, ...styles.creatingBody }, children: showConfiguration ? configurationContent : stepsContent }),
2569
- modalFooter
2570
- ] })
2571
- ] });
3097
+ const bodyStyles = showConfiguration ? styles.body : { ...styles.body, ...styles.creatingBody };
3098
+ const bodyContent = showConfiguration ? configurationContent : stepsContent;
3099
+ const modalContent = /* @__PURE__ */ jsxs3(
3100
+ Modal,
3101
+ {
3102
+ isOpen,
3103
+ onClose,
3104
+ isCentered: true,
3105
+ motionPreset: "scale",
3106
+ closeOnOverlayClick: false,
3107
+ closeOnEsc: false,
3108
+ trapFocus: false,
3109
+ children: [
3110
+ /* @__PURE__ */ jsx3(ModalOverlay, { sx: styles.overlay }),
3111
+ /* @__PURE__ */ jsxs3(ModalContent, { sx: styles.content, children: [
3112
+ /* @__PURE__ */ jsx3(ModalHeader, { sx: styles.header, children: /* @__PURE__ */ jsxs3(HStack2, { justify: "space-between", w: "full", children: [
3113
+ /* @__PURE__ */ jsx3(Text2, { sx: styles.title, children: modalTitle }),
3114
+ /* @__PURE__ */ jsx3(
3115
+ IconButton2,
3116
+ {
3117
+ "aria-label": "Close modal",
3118
+ variant: "ghost",
3119
+ size: "sm",
3120
+ icon: /* @__PURE__ */ jsx3(CloseIcon, {}),
3121
+ onClick: onClose,
3122
+ sx: styles.closeButton
3123
+ }
3124
+ )
3125
+ ] }) }),
3126
+ /* @__PURE__ */ jsx3(ModalBody, { sx: bodyStyles, children: bodyContent }),
3127
+ footerButtons && /* @__PURE__ */ jsx3(ModalFooter, { sx: showFailed ? styles.failedFooter : styles.footer, children: footerButtons })
3128
+ ] })
3129
+ ]
3130
+ }
3131
+ );
3132
+ const drawerFooterNode = footerButtons ? /* @__PURE__ */ jsx3(
3133
+ HStack2,
3134
+ {
3135
+ align: "center",
3136
+ justify: "flex-end",
3137
+ px: 6,
3138
+ h: "68px",
3139
+ bg: "gray.50",
3140
+ borderTop: "1px solid",
3141
+ borderColor: "gray.200",
3142
+ gap: 2,
3143
+ flexShrink: 0,
3144
+ w: "full",
3145
+ children: footerButtons
3146
+ }
3147
+ ) : void 0;
3148
+ const drawerContent = /* @__PURE__ */ jsx3(
3149
+ EtDrawer,
3150
+ {
3151
+ isOpen,
3152
+ onClose,
3153
+ title: modalTitle,
3154
+ closeOnOverlayClick: false,
3155
+ maxW: DRAWER_WIDTH_MAP[drawerSize],
3156
+ bodySx: bodyStyles,
3157
+ footer: drawerFooterNode,
3158
+ children: bodyContent
3159
+ }
3160
+ );
3161
+ const content = displayMode === "drawer" ? drawerContent : modalContent;
2572
3162
  if (theme || cssVarsRoot) {
2573
- return /* @__PURE__ */ jsx3(ChakraProvider, { theme: mergedTheme, cssVarsRoot, children: modalContent });
3163
+ return /* @__PURE__ */ jsx3(ChakraProvider, { theme: mergedTheme, cssVarsRoot, children: content });
2574
3164
  }
2575
- return modalContent;
3165
+ return content;
2576
3166
  }
2577
3167
 
2578
3168
  // src/AutoSchedulerModalWithProvider.tsx
@@ -2648,11 +3238,17 @@ var RequestError = class extends Error {
2648
3238
  export {
2649
3239
  AutoSchedulerModal,
2650
3240
  AutoSchedulerModalWithProvider,
3241
+ InfeasibleScheduleError,
3242
+ MAXIMIZE_CONVERSION_RATE_RULE_ID,
2651
3243
  RequestError,
3244
+ TRAFFIC_FORECAST_INCENTIVE_RULE_ID,
2652
3245
  ViolatedConstraintsPanel,
2653
3246
  ViolatedConstraintsPanelWithProvider,
2654
3247
  buildRulesPayload,
2655
3248
  buildSchedulePayload,
2656
- getFriendlyRuleName
3249
+ defaultSelectedScheduleDates,
3250
+ getFriendlyRuleName,
3251
+ getSequentialWeekDatesFromWeekStart,
3252
+ weekdayTwoLetterFromYmd
2657
3253
  };
2658
3254
  //# sourceMappingURL=index.js.map