506 lines
12 KiB
JavaScript
506 lines
12 KiB
JavaScript
const React = require("react");
|
|
const { Box, Text, useInput, useApp } = require("ink");
|
|
const { useAppState } = require("../../providers/AppProvider.jsx");
|
|
const TextInput = require("ink-text-input").default;
|
|
|
|
/**
|
|
* Scheduling Screen Component
|
|
* Interface for configuring scheduled price update operations
|
|
* Requirements: 7.3, 7.4
|
|
*/
|
|
const SchedulingScreen = () => {
|
|
const { appState, navigateBack, updateConfiguration } = useAppState();
|
|
const { exit } = useApp();
|
|
|
|
// Form fields configuration
|
|
const formFields = [
|
|
{
|
|
id: "scheduledTime",
|
|
label: "Scheduled Execution Time",
|
|
placeholder: "2023-12-25T15:30:00",
|
|
description: "When to run the operation (ISO 8601 format)",
|
|
validator: (value) => {
|
|
if (!value || value.trim() === "") return "Scheduled time is required";
|
|
try {
|
|
const date = new Date(value);
|
|
if (isNaN(date.getTime())) return "Invalid date format";
|
|
if (date <= new Date()) return "Scheduled time must be in the future";
|
|
return null;
|
|
} catch (error) {
|
|
return "Invalid date format";
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: "scheduleType",
|
|
label: "Schedule Type",
|
|
placeholder: "one-time",
|
|
description: "Type of schedule",
|
|
type: "select",
|
|
options: [
|
|
{ value: "one-time", label: "One-time" },
|
|
{ value: "daily", label: "Daily" },
|
|
{ value: "weekly", label: "Weekly" },
|
|
{ value: "monthly", label: "Monthly" },
|
|
],
|
|
},
|
|
{
|
|
id: "scheduleTime",
|
|
label: "Schedule Time",
|
|
placeholder: "15:30",
|
|
description: "Time of day for recurring schedules",
|
|
validator: (value) => {
|
|
if (!value || value.trim() === "") return "Time is required";
|
|
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
|
if (!timeRegex.test(value)) return "Invalid time format (HH:MM)";
|
|
return null;
|
|
},
|
|
},
|
|
];
|
|
|
|
// State for form inputs
|
|
const [formValues, setFormValues] = React.useState(
|
|
formFields.reduce((acc, field) => {
|
|
acc[field.id] = appState.configuration[field.id] || "";
|
|
return acc;
|
|
}, {})
|
|
);
|
|
|
|
const [errors, setErrors] = React.useState({});
|
|
const [focusedField, setFocusedField] = React.useState(0);
|
|
const [showValidation, setShowValidation] = React.useState(false);
|
|
const [nextRunTime, setNextRunTime] = React.useState(null);
|
|
|
|
// Validate all fields
|
|
const validateForm = () => {
|
|
const newErrors = {};
|
|
let isValid = true;
|
|
|
|
formFields.forEach((field) => {
|
|
const error = field.validator(formValues[field.id]);
|
|
if (error) {
|
|
newErrors[field.id] = error;
|
|
isValid = false;
|
|
}
|
|
});
|
|
|
|
setErrors(newErrors);
|
|
return isValid;
|
|
};
|
|
|
|
// Calculate next run time
|
|
const calculateNextRunTime = () => {
|
|
if (!formValues.scheduledTime) return null;
|
|
|
|
try {
|
|
const scheduledDate = new Date(formValues.scheduledTime);
|
|
const now = new Date();
|
|
|
|
// Format for display
|
|
const options = {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
};
|
|
|
|
return scheduledDate.toLocaleString(undefined, options);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Handle keyboard input
|
|
useInput((input, key) => {
|
|
if (key.escape) {
|
|
// Go back to main menu
|
|
navigateBack();
|
|
} else if (key.tab || (key.tab && key.shift)) {
|
|
// Navigate between fields
|
|
if (key.shift) {
|
|
// Shift+Tab - previous field
|
|
setFocusedField((prev) =>
|
|
prev > 0 ? prev - 1 : formFields.length - 1
|
|
);
|
|
} else {
|
|
// Tab - next field
|
|
setFocusedField((prev) =>
|
|
prev < formFields.length - 1 ? prev + 1 : 0
|
|
);
|
|
}
|
|
} else if (key.return || key.enter) {
|
|
// Handle Enter key
|
|
if (focusedField === formFields.length - 1) {
|
|
// Last field (Save button) - save configuration
|
|
handleSave();
|
|
} else {
|
|
// Move to next field
|
|
setFocusedField((prev) =>
|
|
prev < formFields.length - 1 ? prev + 1 : 0
|
|
);
|
|
}
|
|
} else if (key.upArrow) {
|
|
// Navigate up
|
|
setFocusedField((prev) => (prev > 0 ? prev - 1 : formFields.length - 1));
|
|
} else if (key.downArrow) {
|
|
// Navigate down
|
|
setFocusedField((prev) => (prev < formFields.length - 1 ? prev + 1 : 0));
|
|
}
|
|
});
|
|
|
|
// Handle input changes
|
|
const handleInputChange = (fieldId, value) => {
|
|
setFormValues((prev) => ({
|
|
...prev,
|
|
[fieldId]: value,
|
|
}));
|
|
|
|
// Clear error when user starts typing
|
|
if (errors[fieldId]) {
|
|
setErrors((prev) => {
|
|
const newErrors = { ...prev };
|
|
delete newErrors[fieldId];
|
|
return newErrors;
|
|
});
|
|
}
|
|
|
|
// Update next run time when scheduled time changes
|
|
if (fieldId === "scheduledTime") {
|
|
setNextRunTime(calculateNextRunTime());
|
|
}
|
|
};
|
|
|
|
// Handle save configuration
|
|
const handleSave = () => {
|
|
if (validateForm()) {
|
|
// Update configuration
|
|
const config = {
|
|
...formValues,
|
|
isScheduled: true,
|
|
};
|
|
|
|
updateConfiguration(config);
|
|
navigateBack();
|
|
} else {
|
|
// Show validation errors
|
|
setShowValidation(true);
|
|
}
|
|
};
|
|
|
|
// Handle test schedule
|
|
const handleTestSchedule = () => {
|
|
// Validate required fields
|
|
const requiredFields = ["scheduledTime"];
|
|
const tempErrors = {};
|
|
let hasError = false;
|
|
|
|
requiredFields.forEach((fieldId) => {
|
|
const field = formFields.find((f) => f.id === fieldId);
|
|
const error = field.validator(formValues[fieldId]);
|
|
if (error) {
|
|
tempErrors[fieldId] = error;
|
|
hasError = true;
|
|
}
|
|
});
|
|
|
|
if (hasError) {
|
|
setErrors(tempErrors);
|
|
setShowValidation(true);
|
|
return;
|
|
}
|
|
|
|
// Calculate and display next run time
|
|
setNextRunTime(calculateNextRunTime());
|
|
};
|
|
|
|
return React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
|
// Header
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", marginBottom: 2 },
|
|
React.createElement(Text, { bold: true, color: "cyan" }, "⏰ Scheduling"),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray" },
|
|
"Configure when to run price update operations"
|
|
)
|
|
),
|
|
|
|
// Schedule information
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
borderStyle: "single",
|
|
borderColor: "blue",
|
|
paddingX: 1,
|
|
paddingY: 1,
|
|
marginBottom: 2,
|
|
},
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column" },
|
|
React.createElement(
|
|
Text,
|
|
{ bold: true, color: "blue" },
|
|
"Schedule Information:"
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
null,
|
|
` Current Mode: ${appState.configuration.operationMode.toUpperCase()}`
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
null,
|
|
` Target Tag: ${appState.configuration.targetTag || "Not set"}`
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
null,
|
|
` Store: ${appState.configuration.shopDomain || "Not set"}`
|
|
)
|
|
)
|
|
),
|
|
|
|
// Form fields
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", marginBottom: 2 },
|
|
formFields.map((field, index) => {
|
|
const isFocused = focusedField === index;
|
|
const hasError = errors[field.id];
|
|
const currentValue = formValues[field.id];
|
|
|
|
return React.createElement(
|
|
Box,
|
|
{
|
|
key: field.id,
|
|
borderStyle: "single",
|
|
borderColor: hasError ? "red" : isFocused ? "blue" : "gray",
|
|
paddingX: 1,
|
|
paddingY: 1,
|
|
marginBottom: 1,
|
|
flexDirection: "column",
|
|
},
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "row", alignItems: "center", marginBottom: 1 },
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
bold: true,
|
|
color: isFocused ? "blue" : "white",
|
|
},
|
|
`${field.label}:`
|
|
),
|
|
React.createElement(
|
|
Box,
|
|
{ flexGrow: 1 },
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray" },
|
|
` ${field.description}`
|
|
)
|
|
)
|
|
),
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "row" },
|
|
field.type === "select"
|
|
? React.createElement(
|
|
Box,
|
|
{ flexDirection: "column" },
|
|
field.options.map((option, optIndex) =>
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
key: optIndex,
|
|
borderStyle:
|
|
formValues[field.id] === option.value
|
|
? "single"
|
|
: "none",
|
|
borderColor: "blue",
|
|
paddingX: 2,
|
|
paddingY: 0.5,
|
|
marginBottom: 0.5,
|
|
backgroundColor:
|
|
formValues[field.id] === option.value
|
|
? "blue"
|
|
: undefined,
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color:
|
|
formValues[field.id] === option.value
|
|
? "white"
|
|
: "gray",
|
|
},
|
|
option.label
|
|
)
|
|
)
|
|
)
|
|
)
|
|
: React.createElement(TextInput, {
|
|
value: currentValue,
|
|
placeholder: field.placeholder,
|
|
mask: null,
|
|
showCursor: isFocused,
|
|
focus: isFocused,
|
|
onChange: (value) => handleInputChange(field.id, value),
|
|
style: {
|
|
color: hasError ? "red" : isFocused ? "blue" : "white",
|
|
bold: isFocused,
|
|
},
|
|
})
|
|
),
|
|
hasError &&
|
|
React.createElement(
|
|
Text,
|
|
{ color: "red", italic: true },
|
|
` Error: ${errors[field.id]}`
|
|
)
|
|
);
|
|
})
|
|
),
|
|
|
|
// Next run time display
|
|
nextRunTime &&
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
borderStyle: "single",
|
|
borderColor: "green",
|
|
paddingX: 1,
|
|
paddingY: 1,
|
|
marginBottom: 2,
|
|
},
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column" },
|
|
React.createElement(
|
|
Text,
|
|
{ bold: true, color: "green" },
|
|
"Next Scheduled Run:"
|
|
),
|
|
React.createElement(Text, { color: "green" }, ` ${nextRunTime}`)
|
|
)
|
|
),
|
|
|
|
// Action buttons
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "row", justifyContent: "space-between" },
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", width: "48%" },
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
borderStyle: "single",
|
|
borderColor: "gray",
|
|
paddingX: 2,
|
|
paddingY: 1,
|
|
alignItems: "center",
|
|
backgroundColor:
|
|
focusedField === formFields.length ? "yellow" : undefined,
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color: focusedField === formFields.length ? "black" : "white",
|
|
bold: true,
|
|
},
|
|
focusedField === formFields.length ? "Testing..." : "Test Schedule"
|
|
)
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray", italic: true, marginTop: 0.5 },
|
|
"Calculate next run time"
|
|
)
|
|
),
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", width: "48%" },
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
borderStyle: "single",
|
|
borderColor: "green",
|
|
paddingX: 2,
|
|
paddingY: 1,
|
|
alignItems: "center",
|
|
backgroundColor:
|
|
focusedField === formFields.length - 1 ? "green" : undefined,
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color: "white",
|
|
bold: true,
|
|
},
|
|
"Save & Exit"
|
|
)
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray", italic: true, marginTop: 0.5 },
|
|
"Save schedule and return to menu"
|
|
)
|
|
)
|
|
),
|
|
|
|
// Instructions
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
flexDirection: "column",
|
|
marginTop: 2,
|
|
borderTopStyle: "single",
|
|
borderColor: "gray",
|
|
paddingTop: 2,
|
|
},
|
|
React.createElement(Text, { color: "gray" }, "Navigation:"),
|
|
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate fields"),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray" },
|
|
" Tab/Shift+Tab - Next/Previous field"
|
|
),
|
|
React.createElement(Text, { color: "gray" }, " Enter - Save/Next field"),
|
|
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
|
|
),
|
|
|
|
// Validation message
|
|
showValidation &&
|
|
Object.keys(errors).length > 0 &&
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
flexDirection: "column",
|
|
marginTop: 2,
|
|
padding: 1,
|
|
borderStyle: "single",
|
|
borderColor: "red",
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{ color: "red", bold: true },
|
|
"Validation Errors:"
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "red" },
|
|
"Please fix the errors before saving."
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
module.exports = SchedulingScreen;
|