diff --git a/packages/dashboard/src/components/appbar.tsx b/packages/dashboard/src/components/appbar.tsx index 610381eb4..8ba826f44 100644 --- a/packages/dashboard/src/components/appbar.tsx +++ b/packages/dashboard/src/components/appbar.tsx @@ -36,7 +36,7 @@ import { useMediaQuery, } from '@mui/material'; import { styled } from '@mui/system'; -import { AlertRequest, FireAlarmTriggerState, TaskFavorite } from 'api-client'; +import { AlertRequest, FireAlarmTriggerState, RobotTaskRequest, TaskFavorite } from 'api-client'; import { formatDistance } from 'date-fns'; import React from 'react'; import { @@ -194,38 +194,49 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea return () => subs.forEach((s) => s.unsubscribe()); }, [rmf]); - const submitTasks = React.useCallback['submitTasks']>( - async (taskRequests, schedule) => { + const dispatchTask = React.useCallback['dispatchTask']>( + async (taskRequest, robotDispatchTarget) => { if (!rmf) { throw new Error('tasks api not available'); } - if (!schedule) { - await Promise.all( - taskRequests.map((request) => { - console.debug('submitTask:'); - console.debug(request); - return rmf.tasksApi.postDispatchTaskTasksDispatchTaskPost({ - type: 'dispatch_task_request', - request, - }); - }), - ); + if (robotDispatchTarget) { + const robotTask: RobotTaskRequest = { + type: 'robot_task_request', + robot: robotDispatchTarget.robot, + fleet: robotDispatchTarget.fleet, + request: taskRequest, + }; + console.debug(`dispatch robot task:`); + console.debug(robotTask); + await rmf.tasksApi.postRobotTaskTasksRobotTaskPost(robotTask); } else { - const scheduleRequests = taskRequests.map((req) => { - console.debug('schedule task:'); - console.debug(req); - console.debug(schedule); - return toApiSchedule(req, schedule); + console.debug('dispatch task:'); + console.debug(taskRequest); + await rmf.tasksApi.postDispatchTaskTasksDispatchTaskPost({ + type: 'dispatch_task_request', + request: taskRequest, }); - await Promise.all( - scheduleRequests.map((req) => rmf.tasksApi.postScheduledTaskScheduledTasksPost(req)), - ); } AppEvents.refreshTaskApp.next(); }, [rmf], ); + const scheduleTask = React.useCallback['scheduleTask']>( + async (taskRequest, schedule) => { + if (!rmf) { + throw new Error('tasks api not available'); + } + console.debug('schedule task:'); + console.debug(taskRequest); + console.debug(schedule); + const scheduleRequest = toApiSchedule(taskRequest, schedule); + await rmf.tasksApi.postScheduledTaskScheduledTasksPost(scheduleRequest); + AppEvents.refreshTaskApp.next(); + }, + [rmf], + ); + //#region 'Favorite Task' React.useEffect(() => { if (!rmf) { @@ -565,7 +576,8 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea favoritesTasks={favoritesTasks} open={openCreateTaskForm} onClose={() => setOpenCreateTaskForm(false)} - submitTasks={submitTasks} + dispatchTask={dispatchTask} + scheduleTask={scheduleTask} submitFavoriteTask={submitFavoriteTask} deleteFavoriteTask={deleteFavoriteTask} onSuccess={() => { diff --git a/packages/dashboard/src/components/tasks/task-schedule.tsx b/packages/dashboard/src/components/tasks/task-schedule.tsx index cfbeedd5a..86a85063b 100644 --- a/packages/dashboard/src/components/tasks/task-schedule.tsx +++ b/packages/dashboard/src/components/tasks/task-schedule.tsx @@ -9,7 +9,11 @@ import { DayProps } from '@aldabil/react-scheduler/views/Day'; import { MonthProps } from '@aldabil/react-scheduler/views/Month'; import { WeekProps } from '@aldabil/react-scheduler/views/Week'; import { Button, Typography } from '@mui/material'; -import { ScheduledTask, ScheduledTaskScheduleOutput as ApiSchedule } from 'api-client'; +import { + RobotTaskRequest, + ScheduledTask, + ScheduledTaskScheduleOutput as ApiSchedule, +} from 'api-client'; import React from 'react'; import { ConfirmationDialog, @@ -192,17 +196,45 @@ export const TaskSchedule = () => { ); }; - const submitTasks = React.useCallback['submitTasks']>( - async (taskRequests, schedule) => { + const dispatchTask = React.useCallback['dispatchTask']>( + async (taskRequest, robotDispatchTarget) => { + if (!rmf) { + throw new Error('tasks api not available'); + } + if (robotDispatchTarget) { + const robotTask: RobotTaskRequest = { + type: 'robot_task_request', + robot: robotDispatchTarget.robot, + fleet: robotDispatchTarget.fleet, + request: taskRequest, + }; + console.debug(`dispatch robot task:`); + console.debug(robotTask); + await rmf.tasksApi.postRobotTaskTasksRobotTaskPost(robotTask); + } else { + console.debug('dispatch task:'); + console.debug(taskRequest); + await rmf.tasksApi.postDispatchTaskTasksDispatchTaskPost({ + type: 'dispatch_task_request', + request: taskRequest, + }); + } + AppEvents.refreshTaskApp.next(); + }, + [rmf], + ); + + const scheduleTask = React.useCallback['scheduleTask']>( + async (taskRequest, schedule) => { if (!rmf) { throw new Error('tasks api not available'); } - if (!schedule || !currentScheduleTask) { - throw new Error('No schedule or task selected for submission.'); + if (!currentScheduleTask) { + throw new Error('No schedule task selected for submission.'); } - const scheduleRequests = taskRequests.map((req) => toApiSchedule(req, schedule)); + const scheduleRequest = toApiSchedule(taskRequest, schedule); let exceptDate: string | undefined = undefined; if (eventScope === EventScopes.CURRENT) { @@ -212,14 +244,10 @@ export const TaskSchedule = () => { console.debug(`Editing schedule id ${currentScheduleTask.id}`); } - await Promise.all( - scheduleRequests.map((req) => - rmf.tasksApi.updateScheduleTaskScheduledTasksTaskIdUpdatePost( - currentScheduleTask.id, - req, - exceptDate, - ), - ), + await rmf.tasksApi.updateScheduleTaskScheduledTasksTaskIdUpdatePost( + currentScheduleTask.id, + scheduleRequest, + exceptDate, ); setEventScope(EventScopes.CURRENT); @@ -337,7 +365,8 @@ export const TaskSchedule = () => { setEventScope(EventScopes.CURRENT); AppEvents.refreshTaskSchedule.next(); }} - submitTasks={submitTasks} + dispatchTask={dispatchTask} + scheduleTask={scheduleTask} onSuccess={() => { setOpenCreateTaskForm(false); showAlert('success', 'Successfully created task'); diff --git a/packages/react-components/lib/tasks/create-task.stories.tsx b/packages/react-components/lib/tasks/create-task.stories.tsx index f8afe7123..4a8ca6de2 100644 --- a/packages/react-components/lib/tasks/create-task.stories.tsx +++ b/packages/react-components/lib/tasks/create-task.stories.tsx @@ -11,7 +11,8 @@ type Story = StoryObj; export const CreateTask: Story = { args: { - submitTasks: async () => new Promise((res) => setTimeout(res, 500)), + dispatchTask: async () => new Promise((res) => setTimeout(res, 500)), + scheduleTask: async () => new Promise((res) => setTimeout(res, 500)), cleaningZones: ['test_zone_0', 'test_zone_1'], patrolWaypoints: ['test_waypoint_0', 'test_waypoint_1'], pickupPoints: { test_waypoint_0: 'test_waypoint_0' }, diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 549ad08c1..2016192c0 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -28,6 +28,7 @@ import { ListItem, ListItemSecondaryAction, ListItemText, + ListSubheader, MenuItem, Radio, RadioGroup, @@ -291,14 +292,19 @@ const DaySelectorSwitch: React.VFC = ({ disabled, onChan ); }; +export interface RobotDispatchTarget { + fleet: string; + robot: string; +} + export interface CreateTaskFormProps extends Omit { /** * Shows extra UI elements suitable for submittng batched tasks. Default to 'false'. */ user: string; + fleets?: Record; tasksToDisplay?: TaskDefinition[]; - allowBatch?: boolean; cleaningZones?: string[]; patrolWaypoints?: string[]; pickupZones?: string[]; @@ -309,9 +315,10 @@ export interface CreateTaskFormProps scheduleToEdit?: Schedule; // requestTask is provided only when editing a schedule requestTask?: TaskRequest; - submitTasks?(tasks: TaskRequest[], schedule: Schedule | null): Promise; - onSuccess?(tasks: TaskRequest[]): void; - onFail?(error: Error, tasks: TaskRequest[]): void; + dispatchTask(task: TaskRequest, robotDispatchTarget: RobotDispatchTarget | null): Promise; + scheduleTask(task: TaskRequest, schedule: Schedule): Promise; + onSuccess?(task: TaskRequest): void; + onFail?(error: Error, task?: TaskRequest): void; onSuccessFavoriteTask?(message: string, favoriteTask: TaskFavorite): void; onFailFavoriteTask?(error: Error, favoriteTask: TaskFavorite): void; submitFavoriteTask?(favoriteTask: TaskFavorite): Promise; @@ -322,6 +329,10 @@ export interface CreateTaskFormProps export function CreateTaskForm({ user, + fleets = { + f1: ['r11', 'r12'], + f2: ['r21', 'r22'], + }, tasksToDisplay = [ PatrolTaskDefinition, DeliveryTaskDefinition, @@ -339,7 +350,8 @@ export function CreateTaskForm({ favoritesTasks = [], scheduleToEdit, requestTask, - submitTasks, + dispatchTask, + scheduleTask, onClose, onSuccess, onFail, @@ -395,7 +407,7 @@ export function CreateTaskForm({ if (!defaultTaskDescription || !defaultTaskRequest) { // We should never reach this state unless a misconfiguration happened. const err = Error('Default task could not be generated, this might be a configuration error'); - onFail && onFail(err, []); + onFail && onFail(err); console.error(err.message); throw new TypeError(err.message); } @@ -613,7 +625,7 @@ export function CreateTaskForm({ `Failed to retrieve task request category for task [${newTaskDefinitionId}], there might be a misconfiguration.`, ); console.error(err.message); - onFail && onFail(err, []); + onFail && onFail(err); return; } taskRequest.category = category; @@ -631,11 +643,6 @@ export function CreateTaskForm({ // no memo because deps would likely change const handleSubmit = async (scheduling: boolean) => { - if (!submitTasks) { - onSuccess && onSuccess([taskRequest]); - return; - } - const request = { ...taskRequest }; request.requester = user; request.unix_millis_request_time = Date.now(); @@ -647,7 +654,7 @@ export function CreateTaskForm({ request.description = obj; } catch (e) { console.error('Invalid custom compose task description'); - onFail && onFail(e as Error, [request]); + onFail && onFail(e as Error, request); return; } } @@ -681,7 +688,7 @@ export function CreateTaskForm({ const error = Error( `Failed to generate booking label for task request of definition ID: ${taskDefinitionId}`, ); - onFail && onFail(error, [request]); + onFail && onFail(error, request); return; } @@ -701,20 +708,31 @@ export function CreateTaskForm({ try { setSubmitting(true); - await submitTasks([request], scheduling ? schedule : null); + if (scheduling) { + await scheduleTask(request, schedule); + } else { + let robotDispatchTarget: RobotDispatchTarget | null = null; + if (dispatchType === DispatchType.Robot) { + robotDispatchTarget = { + fleet: '', + robot: '', + }; + } + await dispatchTask(request, robotDispatchTarget); + } setSubmitting(false); if (scheduling) { onSuccessScheduling && onSuccessScheduling(); } else { - onSuccess && onSuccess([request]); + onSuccess && onSuccess(request); } } catch (e) { setSubmitting(false); if (scheduling) { onFailScheduling && onFailScheduling(e as Error); } else { - onFail && onFail(e as Error, [request]); + onFail && onFail(e as Error, request); } } }; @@ -803,6 +821,53 @@ export function CreateTaskForm({ }); }; + enum DispatchType { + Automatic = 'Automatic', + Fleet = 'Fleet', + Robot = 'Robot', + } + + const [dispatchType, setDispatchType] = React.useState(DispatchType.Automatic); + const [robotDispatchTarget, setRobotDispatchTarget] = React.useState( + null, + ); + + const handleChangeDispatchType = (ev: React.ChangeEvent) => { + setDispatchType(ev.target.value as DispatchType); + setTaskRequest((prev) => { + return { + ...prev, + fleet_name: undefined, + }; + }); + setRobotDispatchTarget(null); + }; + + const handleDispatchFleetTargetChange = (ev: React.ChangeEvent) => { + console.log('handleDispatchFleetTargetChange called'); + setTaskRequest((prev) => { + return { + ...prev, + fleet_name: ev.target.value.length > 0 ? ev.target.value : undefined, + }; + }); + }; + + const handleDispatchRobotTargetChange = (ev: React.ChangeEvent) => { + let robotFleet: string | null = null; + for (const fleetName of Object.keys(fleets)) { + if (fleets[fleetName].includes(ev.target.value)) { + robotFleet = fleetName; + break; + } + } + if (robotFleet === null) { + console.error(`Failed to find fleet name for robot [${ev.target.value}]`); + return; + } + setRobotDispatchTarget({ fleet: robotFleet, robot: ev.target.value }); + }; + return ( <> + + + + {DispatchType.Automatic} + {DispatchType.Fleet} + {DispatchType.Robot} + + + + {dispatchType === DispatchType.Fleet ? ( + + {Object.keys(fleets).map((fleetName) => { + return ( + + {fleetName} + + ); + })} + + ) : dispatchType === DispatchType.Robot ? ( + + {Object.keys(fleets).map((fleetName) => { + return ( + <> + {fleetName} + {fleets[fleetName].map((robotName) => ( + + {robotName} + + ))} + + ); + })} + + ) : ( + + )} + + + {renderTaskDescriptionForm(taskDefinitionId)}