@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 +167 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +167 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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:
|
|
1372
|
-
requiredCount: (
|
|
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
|
|
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
|
|
1708
|
-
|
|
1709
|
-
|
|
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" :
|
|
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
|
|
2049
|
-
|
|
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
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
(
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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: {
|
|
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
|
),
|