From 228c8d669adaf89b6e79e6d97d9a251f98b58e05 Mon Sep 17 00:00:00 2001 From: Manuel Torres Date: Fri, 17 Apr 2026 17:13:27 -0400 Subject: [PATCH 1/2] done --- clients/mobile/app/create-task-ai.tsx | 269 ++++-------------- clients/mobile/app/create-task-manual.tsx | 101 ++----- .../mobile/components/tasks/TaskFormBody.tsx | 104 +++++++ .../components/tasks/ai-task-edit-sheet.tsx | 180 ++++++++++++ clients/shared/src/index.ts | 1 + 5 files changed, 369 insertions(+), 286 deletions(-) create mode 100644 clients/mobile/components/tasks/TaskFormBody.tsx create mode 100644 clients/mobile/components/tasks/ai-task-edit-sheet.tsx diff --git a/clients/mobile/app/create-task-ai.tsx b/clients/mobile/app/create-task-ai.tsx index eaa52066..0e1c8a25 100644 --- a/clients/mobile/app/create-task-ai.tsx +++ b/clients/mobile/app/create-task-ai.tsx @@ -5,7 +5,6 @@ import { TextInput, Pressable, ScrollView, - Modal, ActivityIndicator, KeyboardAvoidingView, Platform, @@ -13,87 +12,26 @@ import { import { SafeAreaView } from "react-native-safe-area-context"; import { router } from "expo-router"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { - Sparkles, - Send, - ChevronLeft, - NotepadText, - Clock4, - CalendarSync, - Flag, - MapPin, - House, - ChevronRight, -} from "lucide-react-native"; +import { Sparkles, Send, ChevronLeft } from "lucide-react-native"; import { useAPIClient } from "@shared/api/client"; import { getConfig } from "@shared/api/config"; +import { REQUESTS_FEED_QUERY_KEY } from "@shared/api/requests"; import type { GenerateRequestResponse, MakeRequest } from "@shared"; import { SkeletonCard } from "@/components/ui/SkeletonCard"; +import { + AITaskEditSheet, + type GeneratedTask, +} from "@/components/tasks/ai-task-edit-sheet"; +import { Colors } from "@/constants/theme"; -// Token values for JSX props that can't use className (icon colors, placeholder, etc.) -const colors = { - primary: "#15502c", - textSubtle: "#747474", - textSecondary: "#5d5d5d", - strokeSubtle: "#d8d8d8", - white: "#ffffff", -} as const; - -type ScreenState = "idle" | "loading" | "complete"; - -type GeneratedTask = { - name: string; - request_type?: string; - scheduled_time?: string; - reoccurring?: string; - priority?: string; - room_id?: string; - department?: string; - description?: string; - user_id?: string; -}; - -type TaskFieldRowProps = { - icon: React.ReactNode; - label: string; - value?: string; -}; - -function TaskFieldRow({ icon, label, value }: TaskFieldRowProps) { - return ( - - - {icon} - - {label} - - - - {value ? ( - - {value} - - ) : ( - <> - - Select... - - - - )} - - - ); -} +type ScreenState = "idle" | "loading"; export default function CreateTaskAIScreen() { const [query, setQuery] = useState(""); const [submittedQuery, setSubmittedQuery] = useState(""); const [screenState, setScreenState] = useState("idle"); - const [generatedTask, setGeneratedTask] = useState( - null, - ); - const [taskSheetVisible, setTaskSheetVisible] = useState(false); + const [generatedTask, setGeneratedTask] = useState(null); + const [isEditSheetOpen, setIsEditSheetOpen] = useState(false); const api = useAPIClient(); const queryClient = useQueryClient(); @@ -108,68 +46,57 @@ export default function CreateTaskAIScreen() { }, onSuccess: (data) => { const req = data.request; + setGeneratedTask({ name: req?.name ?? "Untitled Task", request_type: req?.request_type, scheduled_time: req?.scheduled_time, + reoccurring: undefined, priority: req?.priority, room_id: req?.room_id, department: req?.department, description: req?.description, user_id: req?.user_id, }); - setScreenState("complete"); - setTaskSheetVisible(true); + + setScreenState("idle"); + setIsEditSheetOpen(true); + }, + onError: () => { + setScreenState("idle"); }, }); const saveMutation = useMutation({ mutationFn: (task: MakeRequest) => api.post("/request", task), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["requests"] }); + queryClient.invalidateQueries({ queryKey: REQUESTS_FEED_QUERY_KEY }); router.back(); }, }); const handleSend = () => { if (!query.trim() || generateMutation.isPending) return; - setSubmittedQuery(query); + + setSubmittedQuery(query.trim()); setQuery(""); setScreenState("loading"); generateMutation.mutate(query.trim()); }; - const handleSaveTask = () => { - if (!generatedTask) return; - const { hotelId } = getConfig(); - saveMutation.mutate({ - hotel_id: hotelId, - name: generatedTask.name, - request_type: generatedTask.request_type, - scheduled_time: generatedTask.scheduled_time, - priority: generatedTask.priority as MakeRequest["priority"], - room_id: generatedTask.room_id, - department: generatedTask.department, - description: generatedTask.description, - user_id: generatedTask.user_id, - }); - }; - - const handleCancelSheet = () => { - setTaskSheetVisible(false); - setScreenState("idle"); + const handleCloseDraft = () => { + setIsEditSheetOpen(false); setGeneratedTask(null); - setSubmittedQuery(""); + setScreenState("idle"); }; return ( - {/* Header */} - + router.back()} className="mr-3"> - + Task Creation @@ -179,44 +106,49 @@ export default function CreateTaskAIScreen() { behavior={Platform.OS === "ios" ? "padding" : "height"} keyboardVerticalOffset={0} > - {/* Content area */} - {/* Submitted query bubble */} {submittedQuery ? ( - - + + {submittedQuery} ) : null} - {/* Loading state */} {screenState === "loading" && ( <> - - + + Creating Task... )} + + {generateMutation.isError && ( + + Something went wrong generating the task. Please try again. + + )} - {/* AI Chat Input */} - - - - + + + + @@ -241,102 +173,13 @@ export default function CreateTaskAIScreen() { - {/* Task Review Sheet */} - - - - {/* Drag handle */} - - - - - - - {/* Title */} - - - {generatedTask?.name} - - - - {/* Fields */} - - } - label="Task Type" - value={generatedTask?.request_type} - /> - } - label="Deadline" - value={generatedTask?.scheduled_time} - /> - } - label="Reoccurring" - value={generatedTask?.reoccurring} - /> - } - label="Priority" - value={generatedTask?.priority} - /> - } - label="Location" - value={generatedTask?.room_id} - /> - } - label="Department" - value={generatedTask?.department} - /> - - {/* Description */} - - - Description - - - {generatedTask?.description ?? "Empty"} - - - - - {/* Actions */} - - - {saveMutation.isPending ? ( - - ) : ( - - Save Task - - )} - - - - Cancel - - - - - - - - + saveMutation.mutate(task)} + savePending={saveMutation.isPending} + /> ); -} +} \ No newline at end of file diff --git a/clients/mobile/app/create-task-manual.tsx b/clients/mobile/app/create-task-manual.tsx index 5f5bd72a..320847ba 100644 --- a/clients/mobile/app/create-task-manual.tsx +++ b/clients/mobile/app/create-task-manual.tsx @@ -2,10 +2,8 @@ import { useState } from "react"; import { View, Text, - TextInput, Pressable, ScrollView, - ActivityIndicator, KeyboardAvoidingView, Platform, } from "react-native"; @@ -13,7 +11,6 @@ import { SafeAreaView } from "react-native-safe-area-context"; import { router } from "expo-router"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { ChevronLeft } from "lucide-react-native"; -import { Colors } from "@/constants/theme"; import { useAPIClient } from "@shared/api/client"; import { getConfig } from "@shared/api/config"; import { REQUESTS_FEED_QUERY_KEY } from "@shared/api/requests"; @@ -29,6 +26,7 @@ import { DepartmentPicker } from "@/components/tasks/department-picker"; import { DeadlinePicker } from "@/components/tasks/deadline-picker"; import { AssigneePicker } from "@/components/tasks/assignee-picker"; import { RoomPicker } from "@/components/tasks/room-picker"; +import { TaskFormBody } from "@/components/tasks/TaskFormBody"; export default function CreateTaskManualScreen() { const [taskName, setTaskName] = useState(""); @@ -73,7 +71,6 @@ export default function CreateTaskManualScreen() { return ( - {/* Header */} router.back()} className="mr-3"> @@ -93,80 +90,38 @@ export default function CreateTaskManualScreen() { contentContainerStyle={{ padding: 24, gap: 24 }} keyboardShouldPersistTaps="handled" > - {/* Task Name Input */} - - - + + setPriority(v ?? "medium")} + /> - {/* Task Fields */} - - setPriority(v ?? "medium")} - /> + - + - + - - - - - {/* Description */} - - - Description - - - - - - {/* Actions */} - - - {saveMutation.isPending ? ( - - ) : ( - - Save Task - - )} - - router.back()} - className="border border-primary rounded h-[39px] items-center justify-center" - > - - Cancel - - - + + + } + onSave={handleSave} + onCancel={() => router.back()} + saveDisabled={!taskName.trim()} + savePending={saveMutation.isPending} + /> ); -} +} \ No newline at end of file diff --git a/clients/mobile/components/tasks/TaskFormBody.tsx b/clients/mobile/components/tasks/TaskFormBody.tsx new file mode 100644 index 00000000..89e44c55 --- /dev/null +++ b/clients/mobile/components/tasks/TaskFormBody.tsx @@ -0,0 +1,104 @@ +import { View, Text, TextInput, Pressable, ActivityIndicator } from "react-native"; +import { Colors } from "@/constants/theme"; + +type TaskFormBodyProps = { + title: string; + onTitleChange?: (value: string) => void; + description: string; + onDescriptionChange?: (value: string) => void; + fields: React.ReactNode; + onSave: () => void; + onCancel: () => void; + saveLabel?: string; + cancelLabel?: string; + saveDisabled?: boolean; + savePending?: boolean; + readOnly?: boolean; +}; + +export function TaskFormBody({ + title, + onTitleChange, + description, + onDescriptionChange, + fields, + onSave, + onCancel, + saveLabel = "Save Task", + cancelLabel = "Cancel", + saveDisabled = false, + savePending = false, + readOnly = false, +}: TaskFormBodyProps) { + return ( + + + {readOnly ? ( + + {title || "Untitled Task"} + + ) : ( + + )} + + + + {fields} + + + + Description + + + {readOnly ? ( + + {description || "Empty"} + + ) : ( + + )} + + + + + + {savePending ? ( + + ) : ( + + {saveLabel} + + )} + + + + + {cancelLabel} + + + + + ); +} \ No newline at end of file diff --git a/clients/mobile/components/tasks/ai-task-edit-sheet.tsx b/clients/mobile/components/tasks/ai-task-edit-sheet.tsx new file mode 100644 index 00000000..03042e1b --- /dev/null +++ b/clients/mobile/components/tasks/ai-task-edit-sheet.tsx @@ -0,0 +1,180 @@ +import { useEffect, useRef, useState } from "react"; +import { View, ScrollView, Modal } from "react-native"; +import type { + Department, + MakeRequest, + MakeRequestPriority, + RoomWithOptionalGuestBooking, + User, +} from "@shared"; +import { + useGetDepartments, + useGetRoomsId, + useGetUsersId, +} from "@shared"; +import { getConfig } from "@shared/api/config"; +import { TaskFormBody } from "@/components/tasks/TaskFormBody"; +import { PriorityPicker } from "@/components/tasks/priority-picker"; +import { DepartmentPicker } from "@/components/tasks/department-picker"; +import { DeadlinePicker } from "@/components/tasks/deadline-picker"; +import { AssigneePicker } from "@/components/tasks/assignee-picker"; +import { RoomPicker } from "@/components/tasks/room-picker"; + +export type GeneratedTask = { + name: string; + request_type?: string; + scheduled_time?: string; + reoccurring?: string; + priority?: string; + room_id?: string; + department?: string; + description?: string; + user_id?: string; +}; + +type AITaskEditSheetProps = { + visible: boolean; + task: GeneratedTask | null; + onClose: () => void; + onSave: (task: MakeRequest) => void; + savePending?: boolean; + initialDepartment?: Department; + initialAssignee?: User; + initialRoom?: RoomWithOptionalGuestBooking; +}; + +export function AITaskEditSheet({ + visible, + task, + onClose, + onSave, + savePending = false, + initialDepartment, + initialAssignee, + initialRoom, +}: AITaskEditSheetProps) { + const [taskName, setTaskName] = useState(""); + const [description, setDescription] = useState(""); + const [priority, setPriority] = useState("medium"); + const [department, setDepartment] = useState( + initialDepartment, + ); + const [deadline, setDeadline] = useState(undefined); + const [assignee, setAssignee] = useState(initialAssignee); + const [room, setRoom] = useState( + initialRoom, + ); + + const { hotelId } = getConfig(); + + const { data: departments } = useGetDepartments(hotelId); + const resolvedDepartment = departments?.find((d) => d.id === task?.department); + + const { data: resolvedUser } = useGetUsersId(task?.user_id ?? "", { + query: { enabled: !!task?.user_id }, + }); + + const { data: resolvedRoom } = useGetRoomsId(task?.room_id ?? "", { + query: { enabled: !!task?.room_id }, + }); + + // Track whether we've fully initialized for the current task so re-renders + // caused by resolving fetches don't override the user's picker changes. + const lastInitTaskRef = useRef(null); + + useEffect(() => { + if (!task || !visible) { + lastInitTaskRef.current = null; + return; + } + if (lastInitTaskRef.current === task) return; + + setTaskName(task.name ?? ""); + setDescription(task.description ?? ""); + setPriority((task.priority as MakeRequestPriority) ?? "medium"); + setDeadline(task.scheduled_time ? new Date(task.scheduled_time) : undefined); + setDepartment(resolvedDepartment ?? initialDepartment); + setAssignee(resolvedUser ?? initialAssignee); + setRoom(resolvedRoom ?? initialRoom); + + // Mark fully initialized only once all expected lookups have resolved, + // so the effect keeps running until all pickers have their objects. + const allReady = + (!task.department || resolvedDepartment != null) && + (!task.user_id || resolvedUser != null) && + (!task.room_id || resolvedRoom != null); + + if (allReady) { + lastInitTaskRef.current = task; + } + }, [task, visible, resolvedDepartment, resolvedUser, resolvedRoom, initialDepartment, initialAssignee, initialRoom]); + + const handleSave = () => { + if (!taskName.trim()) return; + + const { hotelId } = getConfig(); + + onSave({ + hotel_id: hotelId, + name: taskName.trim(), + description: description.trim() || undefined, + priority, + department: department?.id ?? task?.department, + user_id: assignee?.id ?? task?.user_id, + room_id: room?.id ?? task?.room_id, + scheduled_time: deadline?.toISOString(), + status: "pending", + request_type: task?.request_type ?? "general", + }); + }; + + return ( + + + + + + + + + + setPriority(v ?? "medium")} + /> + + + + + + + + + + } + onSave={handleSave} + onCancel={onClose} + saveDisabled={!taskName.trim()} + savePending={savePending} + /> + + + + + ); +} \ No newline at end of file diff --git a/clients/shared/src/index.ts b/clients/shared/src/index.ts index a774cc60..cc4547a8 100644 --- a/clients/shared/src/index.ts +++ b/clients/shared/src/index.ts @@ -92,6 +92,7 @@ export type { export { usePostRooms, + useGetRoomsId, useGetRoomsFloors, // Hook variants usePostRoomsHook, From dbb3238e924e71c44123e90e79905f6609e2377b Mon Sep 17 00:00:00 2001 From: Manuel Torres Date: Fri, 17 Apr 2026 18:46:09 -0400 Subject: [PATCH 2/2] lint --- clients/mobile/app/create-task-ai.tsx | 6 +++-- clients/mobile/app/create-task-manual.tsx | 2 +- .../mobile/components/tasks/TaskFormBody.tsx | 10 +++++-- .../components/tasks/ai-task-edit-sheet.tsx | 27 ++++++++++++------- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/clients/mobile/app/create-task-ai.tsx b/clients/mobile/app/create-task-ai.tsx index 0e1c8a25..0a642e4a 100644 --- a/clients/mobile/app/create-task-ai.tsx +++ b/clients/mobile/app/create-task-ai.tsx @@ -30,7 +30,9 @@ export default function CreateTaskAIScreen() { const [query, setQuery] = useState(""); const [submittedQuery, setSubmittedQuery] = useState(""); const [screenState, setScreenState] = useState("idle"); - const [generatedTask, setGeneratedTask] = useState(null); + const [generatedTask, setGeneratedTask] = useState( + null, + ); const [isEditSheetOpen, setIsEditSheetOpen] = useState(false); const api = useAPIClient(); @@ -182,4 +184,4 @@ export default function CreateTaskAIScreen() { /> ); -} \ No newline at end of file +} diff --git a/clients/mobile/app/create-task-manual.tsx b/clients/mobile/app/create-task-manual.tsx index 320847ba..bdcf7bce 100644 --- a/clients/mobile/app/create-task-manual.tsx +++ b/clients/mobile/app/create-task-manual.tsx @@ -124,4 +124,4 @@ export default function CreateTaskManualScreen() { ); -} \ No newline at end of file +} diff --git a/clients/mobile/components/tasks/TaskFormBody.tsx b/clients/mobile/components/tasks/TaskFormBody.tsx index 89e44c55..f61ac8c8 100644 --- a/clients/mobile/components/tasks/TaskFormBody.tsx +++ b/clients/mobile/components/tasks/TaskFormBody.tsx @@ -1,4 +1,10 @@ -import { View, Text, TextInput, Pressable, ActivityIndicator } from "react-native"; +import { + View, + Text, + TextInput, + Pressable, + ActivityIndicator, +} from "react-native"; import { Colors } from "@/constants/theme"; type TaskFormBodyProps = { @@ -101,4 +107,4 @@ export function TaskFormBody({ ); -} \ No newline at end of file +} diff --git a/clients/mobile/components/tasks/ai-task-edit-sheet.tsx b/clients/mobile/components/tasks/ai-task-edit-sheet.tsx index 03042e1b..ae682400 100644 --- a/clients/mobile/components/tasks/ai-task-edit-sheet.tsx +++ b/clients/mobile/components/tasks/ai-task-edit-sheet.tsx @@ -7,11 +7,7 @@ import type { RoomWithOptionalGuestBooking, User, } from "@shared"; -import { - useGetDepartments, - useGetRoomsId, - useGetUsersId, -} from "@shared"; +import { useGetDepartments, useGetRoomsId, useGetUsersId } from "@shared"; import { getConfig } from "@shared/api/config"; import { TaskFormBody } from "@/components/tasks/TaskFormBody"; import { PriorityPicker } from "@/components/tasks/priority-picker"; @@ -68,7 +64,9 @@ export function AITaskEditSheet({ const { hotelId } = getConfig(); const { data: departments } = useGetDepartments(hotelId); - const resolvedDepartment = departments?.find((d) => d.id === task?.department); + const resolvedDepartment = departments?.find( + (d) => d.id === task?.department, + ); const { data: resolvedUser } = useGetUsersId(task?.user_id ?? "", { query: { enabled: !!task?.user_id }, @@ -92,7 +90,9 @@ export function AITaskEditSheet({ setTaskName(task.name ?? ""); setDescription(task.description ?? ""); setPriority((task.priority as MakeRequestPriority) ?? "medium"); - setDeadline(task.scheduled_time ? new Date(task.scheduled_time) : undefined); + setDeadline( + task.scheduled_time ? new Date(task.scheduled_time) : undefined, + ); setDepartment(resolvedDepartment ?? initialDepartment); setAssignee(resolvedUser ?? initialAssignee); setRoom(resolvedRoom ?? initialRoom); @@ -107,7 +107,16 @@ export function AITaskEditSheet({ if (allReady) { lastInitTaskRef.current = task; } - }, [task, visible, resolvedDepartment, resolvedUser, resolvedRoom, initialDepartment, initialAssignee, initialRoom]); + }, [ + task, + visible, + resolvedDepartment, + resolvedUser, + resolvedRoom, + initialDepartment, + initialAssignee, + initialRoom, + ]); const handleSave = () => { if (!taskName.trim()) return; @@ -177,4 +186,4 @@ export function AITaskEditSheet({ ); -} \ No newline at end of file +}