diff options
Diffstat (limited to 'jb-ui/src/components/Profiling.tsx')
| -rw-r--r-- | jb-ui/src/components/Profiling.tsx | 1313 |
1 files changed, 676 insertions, 637 deletions
diff --git a/jb-ui/src/components/Profiling.tsx b/jb-ui/src/components/Profiling.tsx index 9cefdda..b834c9f 100644 --- a/jb-ui/src/components/Profiling.tsx +++ b/jb-ui/src/components/Profiling.tsx @@ -1,536 +1,572 @@ -import { - UpkQuestionChoice -} from "@/api_fsb"; -import { - ChevronRightIcon -} from "lucide-react"; +import { UpkQuestionChoice } from "@/api_fsb"; +import { useGetMeQuery } from "@/models/authAPI"; +import { ChevronRightIcon } from "lucide-react"; import { KeyboardEvent, MouseEvent, useEffect, useRef, useState } from "react"; -import { useAppDispatch, useAppSelector } from "@/hooks"; +import { useAppDispatch } from "@/hooks"; import { bpid } from "@/lib/utils"; import { - createColumnHelper, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getSortedRowModel, - RowModel, - RowSelectionState, - useReactTable, -} from '@tanstack/react-table'; + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + RowModel, + RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; import { clsx } from "clsx"; import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, } from "@/components/ui/pagination"; import { - addAnswers, - fetchNewQuestions, - goToNextQuestion, - ProfileQuestion, - questionsSelectors, - saveAnswer, - selectActiveQuestion, - selectActiveQuestionValidation, - selectAllQuestions, - setActiveQuestion + addAnswers, + fetchNewQuestions, + goToNextQuestion, + ProfileQuestion, + questionsSelectors, + saveAnswer, + selectActiveQuestion, + selectActiveQuestionValidation, + selectAllQuestions, + setActiveQuestion, } from "@/models/profilingQuestionsSlice"; import { useSelector } from "react-redux"; // import {motion} from "framer-motion" const TextEntry = () => { - const dispatch = useAppDispatch() - - const activeQuestion = useSelector(selectActiveQuestion); - // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); - // const selectAnswer = useSelector(selectAnswerForQuestion(question)); - const metadata = activeQuestion?._metadata - const validation = activeQuestion?._validation - const bpuid = useAppSelector(state => state.app.bpuid) - - if (!metadata) return null - if (!validation) return null - - const error = validation?.errors.length > 0 - - const handleInputChange = (event: React.KeyboardEvent<HTMLInputElement>) => { - if (!activeQuestion.question_id) return; - - const target = event.target as HTMLInputElement; - dispatch(addAnswers({ questionId: activeQuestion.question_id!, answers: [target.value] as string[] })) - }; - - const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => { - if (!activeQuestion.question_id) return; - if (!bpuid) return; - - if (event.key === 'Enter') { - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } - }; - - return ( - <div className="px-6 my-6"> - <input type="text" - id="text-entry-input" - aria-describedby="" - // defaultValue={answer?.values.length ? answer?.values[0] : ""} - onKeyUp={handleInputChange} - onKeyDown={handleKeyPress} // Use onKeyDown to listen for the key press - // title={error ? answer?.error_msg : ""} - className={clsx( - 'w-full border-1 rounded px-2 py-1', - error ? - 'border-red-500 focus-visible:ring-red-500' : - 'border-blue-50' - )} - /> - - { - validation.errors.map(e => { - return ( - <p className="text-sm text-red-100 my-2">{e.message}</p> - ) - }) - } - </div> - ) -} - - -export default function useKeyboardMode( - rowModel: RowModel<UpkQuestionChoice>, -) { - // const [keyboardEnabled, setKeyboardEnabled] = useState( - // localStorage.getItem(keyboardMode) === "enabled", - // ); - const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]); - - const dispatch = useAppDispatch() - const activeQuestion = useSelector(selectActiveQuestion); - const bpuid = useAppSelector(state => state.app.bpuid) - - const keyboardSelectionIdxRef = useRef(0); - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - - if (!activeQuestion) return; - if (!bpuid) return; - - // if (!keyboardEnabled) { - // return; - // } - - const { key } = event; - const totalRows = rowRefs.current.length; - const prevIdx = keyboardSelectionIdxRef.current; - let nextIdx = prevIdx; - - if (key === "ArrowDown") nextIdx = Math.min(prevIdx + 1, totalRows - 1); - if (key === "ArrowUp") nextIdx = Math.max(prevIdx - 1, 0); - if (key === " ") { - const active = document.activeElement as HTMLElement | null; - - if (active?.tabIndex === 0) { - return; - } else { - rowModel.rows[keyboardSelectionIdxRef.current].toggleSelected(); - } - } + const dispatch = useAppDispatch(); + + const activeQuestion = useSelector(selectActiveQuestion); + // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); + // const selectAnswer = useSelector(selectAnswerForQuestion(question)); + const metadata = activeQuestion?._metadata; + const validation = activeQuestion?._validation; + const { data: user } = useGetMeQuery(); + + if (!metadata) return null; + if (!validation) return null; + + const error = validation?.errors.length > 0; + + const handleInputChange = (event: React.KeyboardEvent<HTMLInputElement>) => { + if (!activeQuestion.question_id) return; + + const target = event.target as HTMLInputElement; + dispatch( + addAnswers({ + questionId: activeQuestion.question_id!, + answers: [target.value] as string[], + }), + ); + }; + + const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => { + if (!activeQuestion.question_id) return; + if (!user?.product_user_id) return; + + if (event.key === "Enter") { + dispatch( + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), + ); + } + }; + + return ( + <div className="px-6 my-6"> + <input + type="text" + id="text-entry-input" + aria-describedby="" + // defaultValue={answer?.values.length ? answer?.values[0] : ""} + onKeyUp={handleInputChange} + onKeyDown={handleKeyPress} // Use onKeyDown to listen for the key press + // title={error ? answer?.error_msg : ""} + className={clsx( + "w-full border-1 rounded px-2 py-1", + error + ? "border-red-500 focus-visible:ring-red-500" + : "border-blue-50", + )} + /> - if (event.key === 'Enter') { - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } + {validation.errors.map((e) => { + return <p className="text-sm text-red-100 my-2">{e.message}</p>; + })} + </div> + ); +}; +export default function useKeyboardMode(rowModel: RowModel<UpkQuestionChoice>) { + // const [keyboardEnabled, setKeyboardEnabled] = useState( + // localStorage.getItem(keyboardMode) === "enabled", + // ); + const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]); - if (nextIdx !== prevIdx) { + const dispatch = useAppDispatch(); + const activeQuestion = useSelector(selectActiveQuestion); + const { data: user } = useGetMeQuery(); - rowRefs.current[prevIdx]?.classList.remove("bg-blue-400"); - rowRefs.current[nextIdx]?.classList.add("bg-blue-400"); + const keyboardSelectionIdxRef = useRef(0); - rowRefs.current[nextIdx]?.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - keyboardSelectionIdxRef.current = nextIdx; - } - }; + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!activeQuestion) return; + if (!user?.product_user_id) return; - // I really don't know, and I don't really care enough to make this - // technically type'd correctly. - Max + // if (!keyboardEnabled) { + // return; + // } - // @ts-ignore - window.addEventListener("keydown", handleKeyDown); - // @ts-ignore - return () => window.removeEventListener("keydown", handleKeyDown); + const { key } = event; + const totalRows = rowRefs.current.length; + const prevIdx = keyboardSelectionIdxRef.current; + let nextIdx = prevIdx; - }, [rowModel.rows]); + if (key === "ArrowDown") nextIdx = Math.min(prevIdx + 1, totalRows - 1); + if (key === "ArrowUp") nextIdx = Math.max(prevIdx - 1, 0); + if (key === " ") { + const active = document.activeElement as HTMLElement | null; - return { - // keyboardEnabled, - // setKeyboardEnabled, - rowRefs, - keyboardSelectionIdxRef, + if (active?.tabIndex === 0) { + return; + } else { + rowModel.rows[keyboardSelectionIdxRef.current].toggleSelected(); + } + } + + if (event.key === "Enter") { + dispatch( + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), + ); + } + + if (nextIdx !== prevIdx) { + rowRefs.current[prevIdx]?.classList.remove("bg-blue-400"); + rowRefs.current[nextIdx]?.classList.add("bg-blue-400"); + + rowRefs.current[nextIdx]?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + keyboardSelectionIdxRef.current = nextIdx; + } }; + + // I really don't know, and I don't really care enough to make this + // technically type'd correctly. - Max + + // @ts-ignore + window.addEventListener("keydown", handleKeyDown); + // @ts-ignore + return () => window.removeEventListener("keydown", handleKeyDown); + }, [rowModel.rows]); + + return { + // keyboardEnabled, + // setKeyboardEnabled, + rowRefs, + keyboardSelectionIdxRef, + }; } const MultipleChoice = () => { - const dispatch = useAppDispatch() - const activeQuestion = useSelector(selectActiveQuestion); - - // const error: Boolean = answer?.error_msg.length > 0 - // console.log("Rendering MultipleChoice with answer:", answer, error) - - // TODO! Retrieve store values to use as the initial state for the table - // This is useful for if they tab around before submitting the answer - const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); - const [globalFilter, setGlobalFilter] = useState(''); - - const handleRowSelectionChange = (updaterOrValue: RowSelectionState | ((old: RowSelectionState) => RowSelectionState)) => { - setRowSelection((oldSelection) => { - // Get the new selection state - const newSelection = typeof updaterOrValue === 'function' - ? updaterOrValue(oldSelection) - : updaterOrValue; - - // Find which row was just selected/deselected - const changedRowId = Object.keys(newSelection).find( - key => newSelection[key] !== oldSelection[key] - ); - - if (!changedRowId) return newSelection; - - // Get the actual row data - const row = table.getRow(changedRowId); - const rowData = row.original; // Your row data object - - // Check if "None of the Above" was selected - const normalized = rowData.choice_text.trim().toLowerCase(); - const isNoneOfTheAbove = (normalized === "none of the above") || (rowData.exclusive ?? false); - if (isNoneOfTheAbove && newSelection[changedRowId]) { - // Clear all other selections, keep only this one - return { [changedRowId]: true }; - } - - // Check if a regular option was selected while "None of the Above" exists - const noneOfTheAboveId = Object.keys(newSelection).find(id => { - const r = table.getRow(id); - const rowData = r.original as UpkQuestionChoice; - - const normalized = rowData.choice_text.trim().toLowerCase(); - const isNoneOfTheAbove = (normalized === "none of the above") || (rowData.exclusive ?? false); - - return isNoneOfTheAbove && newSelection[id]; - }); - - if (noneOfTheAboveId && changedRowId !== noneOfTheAboveId) { - // Remove "None of the Above" selection - const { [noneOfTheAboveId]: _, ...rest } = newSelection; - return rest; - } - - return newSelection; - }); - }; + const dispatch = useAppDispatch(); + const activeQuestion = useSelector(selectActiveQuestion); + + // const error: Boolean = answer?.error_msg.length > 0 + // console.log("Rendering MultipleChoice with answer:", answer, error) + + // TODO! Retrieve store values to use as the initial state for the table + // This is useful for if they tab around before submitting the answer + const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); + const [globalFilter, setGlobalFilter] = useState(""); + + const handleRowSelectionChange = ( + updaterOrValue: + | RowSelectionState + | ((old: RowSelectionState) => RowSelectionState), + ) => { + setRowSelection((oldSelection) => { + // Get the new selection state + const newSelection = + typeof updaterOrValue === "function" + ? updaterOrValue(oldSelection) + : updaterOrValue; + + // Find which row was just selected/deselected + const changedRowId = Object.keys(newSelection).find( + (key) => newSelection[key] !== oldSelection[key], + ); + + if (!changedRowId) return newSelection; + + // Get the actual row data + const row = table.getRow(changedRowId); + const rowData = row.original; // Your row data object + + // Check if "None of the Above" was selected + const normalized = rowData.choice_text.trim().toLowerCase(); + const isNoneOfTheAbove = + normalized === "none of the above" || (rowData.exclusive ?? false); + if (isNoneOfTheAbove && newSelection[changedRowId]) { + // Clear all other selections, keep only this one + return { [changedRowId]: true }; + } + + // Check if a regular option was selected while "None of the Above" exists + const noneOfTheAboveId = Object.keys(newSelection).find((id) => { + const r = table.getRow(id); + const rowData = r.original as UpkQuestionChoice; + + const normalized = rowData.choice_text.trim().toLowerCase(); + const isNoneOfTheAbove = + normalized === "none of the above" || (rowData.exclusive ?? false); + + return isNoneOfTheAbove && newSelection[id]; + }); + + if (noneOfTheAboveId && changedRowId !== noneOfTheAboveId) { + // Remove "None of the Above" selection + const { [noneOfTheAboveId]: _, ...rest } = newSelection; + return rest; + } + + return newSelection; + }); + }; + + useEffect(() => { + if (!activeQuestion?.question_id) return; + // Run this anytime a rowSelection changes, to update + // the answer in the store + const selectedChoices = Object.keys(rowSelection); + dispatch( + addAnswers({ + questionId: activeQuestion.question_id, + answers: selectedChoices, + }), + ); + }, [rowSelection]); + + // Define columns + const columnHelper = createColumnHelper<UpkQuestionChoice>(); + const columns = [ + columnHelper.display({ + id: "select", + cell: ({ row }) => { + const canSelected = row.getCanSelect(); - useEffect(() => { - if (!activeQuestion?.question_id) return; - // Run this anytime a rowSelection changes, to update - // the answer in the store - const selectedChoices = Object.keys(rowSelection) - dispatch(addAnswers({ questionId: activeQuestion.question_id, answers: selectedChoices })) - }, [rowSelection]) - - // Define columns - const columnHelper = createColumnHelper<UpkQuestionChoice>() - const columns = [ - columnHelper.display({ - id: 'select', - cell: ({ row }) => { - const canSelected = row.getCanSelect() - - return ( - <input - type="checkbox" - checked={row.getIsSelected()} - disabled={!row.getCanSelect()} - onChange={row.getToggleSelectedHandler()} - className={`w-4 h-4 rounded-0 border-slate-300 cursor-pointer + return ( + <input + type="checkbox" + checked={row.getIsSelected()} + disabled={!row.getCanSelect()} + onChange={row.getToggleSelectedHandler()} + className={`w-4 h-4 rounded-0 border-slate-300 cursor-pointer focus:outline-none focus:border-blue-500 focus:ring-blue-500 - ${canSelected ? ' ' : 'hidden'} + ${canSelected ? " " : "hidden"} `} - /> - ) - }, - size: 20, - meta: { - align: 'center' - } - }), - - columnHelper.accessor("choice_text", { - header: () => 'Choice Text', - cell: (info) => { - const text = info.getValue(); - const selected = info.row.getIsSelected() - const canSelected = info.row.getCanSelect() + /> + ); + }, + size: 20, + meta: { + align: "center", + }, + }), + + columnHelper.accessor("choice_text", { + header: () => "Choice Text", + cell: (info) => { + const text = info.getValue(); + const selected = info.row.getIsSelected(); + const canSelected = info.row.getCanSelect(); - return ( - <p className={` + return ( + <p + className={` w-max truncate text-sm text-left pl-1 - ${selected ? 'font-bold' : 'font-medium'} - ${canSelected ? 'text-blue-950' : 'text-blue-600'} - `}> - {text} - </p> - ) - }, - size: 380, - meta: { - align: 'left' - } - }), - ] - - const table = useReactTable({ - 'data': activeQuestion!.choices ?? [], - columns, - getRowId: (row) => row.choice_id, - state: { - rowSelection, - globalFilter, - }, - // enableRowSelection: true, - enableRowSelection: (row) => { - // Allow deselection always, but limit new selections - const currentSelectionCount = Object.keys(rowSelection).length; - const isRowSelected = rowSelection[row.id]; - - const max_selection = activeQuestion?.selector === "SA" ? 1 : activeQuestion?.choices?.length ?? 10 - - // Allow if: row is already selected OR we haven't hit the limit - return isRowSelected || currentSelectionCount < max_selection; - }, - onRowSelectionChange: handleRowSelectionChange, - onGlobalFilterChange: setGlobalFilter, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - }); - - const selectedCount = table.getSelectedRowModel().rows.length; - - //const { rowRefs, setKeyboardEnabled, keyboardSelectionIdxRef } = useKeyboardMode(table.getRowModel()); - const { rowRefs } = useKeyboardMode(table.getRowModel()); - - return ( - <> - - {/* Controls */} - <div className="py-1"> - <div className="flex flex-wrap gap-2"> - - {/* Search */} - <div className="flex-1"> - <input - type="text" - placeholder="Search choices..." - value={globalFilter ?? ''} - onChange={e => setGlobalFilter(e.target.value)} - className="w-full px-4 py-1 + ${selected ? "font-bold" : "font-medium"} + ${canSelected ? "text-blue-950" : "text-blue-600"} + `} + > + {text} + </p> + ); + }, + size: 380, + meta: { + align: "left", + }, + }), + ]; + + const table = useReactTable({ + data: activeQuestion!.choices ?? [], + columns, + getRowId: (row) => row.choice_id, + state: { + rowSelection, + globalFilter, + }, + // enableRowSelection: true, + enableRowSelection: (row) => { + // Allow deselection always, but limit new selections + const currentSelectionCount = Object.keys(rowSelection).length; + const isRowSelected = rowSelection[row.id]; + + const max_selection = + activeQuestion?.selector === "SA" + ? 1 + : (activeQuestion?.choices?.length ?? 10); + + // Allow if: row is already selected OR we haven't hit the limit + return isRowSelected || currentSelectionCount < max_selection; + }, + onRowSelectionChange: handleRowSelectionChange, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + + const selectedCount = table.getSelectedRowModel().rows.length; + + //const { rowRefs, setKeyboardEnabled, keyboardSelectionIdxRef } = useKeyboardMode(table.getRowModel()); + const { rowRefs } = useKeyboardMode(table.getRowModel()); + + return ( + <> + {/* Controls */} + <div className="py-1"> + <div className="flex flex-wrap gap-2"> + {/* Search */} + <div className="flex-1"> + <input + type="text" + placeholder="Search choices..." + value={globalFilter ?? ""} + onChange={(e) => setGlobalFilter(e.target.value)} + className="w-full px-4 py-1 border border-blue-900 bg-blue-400 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" - /> - </div> + /> + </div> - {/* Selected Count */} - {selectedCount > 0 && ( - <div className="flex items-center gap-2 px-2 py-1 + {/* Selected Count */} + {selectedCount > 0 && ( + <div + className="flex items-center gap-2 px-2 py-1 border border-blue-900 text-blue-700 rounded font-medium text-[10px] hover:cursor-pointer hover:bg-blue-800 hover:text-white " - onClick={() => setRowSelection({})}> - <span>{selectedCount} selected</span> - <button className="m-0 p-0 font-bold">✕</button> - </div> - )} - - </div> + onClick={() => setRowSelection({})} + > + <span>{selectedCount} selected</span> + <button className="m-0 p-0 font-bold">✕</button> </div> + )} + </div> + </div> - {/* Table */} - <div className="border border-blue-200 rounded max-h-[400px] overflow-y-auto + {/* Table */} + <div + className="border border-blue-200 rounded max-h-[400px] overflow-y-auto [&::-webkit-scrollbar]:h-[4px] [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-blue-100 [&::-webkit-scrollbar-thumb]:bg-blue-300 - "> - <table className="text-xs table-fixed border-collapse w-full"> - <tbody className="w-full"> - {table.getRowModel().rows.map((row, idx) => ( - <tr key={row.id} - ref={(el) => { - rowRefs.current[idx] = el; - }} - onClick={() => row.toggleSelected()} - className={`cursor-pointer transition-colors w-full - ${row.getIsSelected() ? 'bg-blue-600 ' : ' ' + " + > + <table className="text-xs table-fixed border-collapse w-full"> + <tbody className="w-full"> + {table.getRowModel().rows.map((row, idx) => ( + <tr + key={row.id} + ref={(el) => { + rowRefs.current[idx] = el; + }} + onClick={() => row.toggleSelected()} + className={`cursor-pointer transition-colors w-full + ${ + row.getIsSelected() ? "bg-blue-600 " : " " }`} - > - - {row.getVisibleCells().map((cell) => { - const align = cell.column.columnDef.meta?.align || 'left'; - const alignClass = align === 'center' ? 'text-center' : - align === 'right' ? 'text-right' : 'text-left'; - - return ( - <td key={cell.id} - className={`py-2 ${alignClass}`} - style={{ - width: `${cell.column.getSize()}px`, - }} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - </td> - ) - })} - - </tr> - ))} - </tbody> - </table> - </div> - - {/* Empty State */} - {table.getRowModel().rows.length === 0 && ( - <div className="p-12 text-center text-blue-50 font-bold"> - No items found matching your search - </div> - )} + > + {row.getVisibleCells().map((cell) => { + const align = cell.column.columnDef.meta?.align || "left"; + const alignClass = + align === "center" + ? "text-center" + : align === "right" + ? "text-right" + : "text-left"; + + return ( + <td + key={cell.id} + className={`py-2 ${alignClass}`} + style={{ + width: `${cell.column.getSize()}px`, + }} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + </td> + ); + })} + </tr> + ))} + </tbody> + </table> + </div> + + {/* Empty State */} + {table.getRowModel().rows.length === 0 && ( + <div className="p-12 text-center text-blue-50 font-bold"> + No items found matching your search + </div> + )} - {/* Footer */} - <div className="px-2 py-2 flex justify-between items-center + {/* Footer */} + <div + className="px-2 py-2 flex justify-between items-center text-xs font-mono font-semibold text-blue-950 - "> - <span> - Showing {table.getRowModel().rows.length} of {activeQuestion?.choices?.length} choices - </span> - {selectedCount > 0 && ( - <span> - {selectedCount} choice{selectedCount !== 1 ? 's' : ''} selected - </span> - )} - </div> - - </> - ) - -} + " + > + <span> + Showing {table.getRowModel().rows.length} of{" "} + {activeQuestion?.choices?.length} choices + </span> + {selectedCount > 0 && ( + <span> + {selectedCount} choice{selectedCount !== 1 ? "s" : ""} selected + </span> + )} + </div> + </> + ); +}; const ProfilingQuestionTitle = () => { - const activeQuestion = useSelector(selectActiveQuestion); - const question_active_tasks = activeQuestion?.importance?.task_count ?? 0 + const activeQuestion = useSelector(selectActiveQuestion); + const question_active_tasks = activeQuestion?.importance?.task_count ?? 0; - if (!activeQuestion) return null; - - let text = activeQuestion?.question_text + if (!activeQuestion) return null; - if (activeQuestion.explanation_template && activeQuestion._validation?.isComplete && activeQuestion._metadata.answers.length == 1) { - const template = activeQuestion.explanation_template; - const pre_code = activeQuestion._metadata.answers[0]; + let text = activeQuestion?.question_text; - const choice = activeQuestion.choices?.find(c => c.choice_id === pre_code) - const choice_text = choice ? choice.choice_text : "your answer" + if ( + activeQuestion.explanation_template && + activeQuestion._validation?.isComplete && + activeQuestion._metadata.answers.length == 1 + ) { + const template = activeQuestion.explanation_template; + const pre_code = activeQuestion._metadata.answers[0]; - text = template.replace("{answer}", choice_text) - } + const choice = activeQuestion.choices?.find( + (c) => c.choice_id === pre_code, + ); + const choice_text = choice ? choice.choice_text : "your answer"; + text = template.replace("{answer}", choice_text); + } - return ( - <div className="flex justify-between items-center px-2"> - - <h3 className="text-left text-sm font-bold m-0 p-0 + return ( + <div className="flex justify-between items-center px-2"> + <h3 + className="text-left text-sm font-bold m-0 p-0 inline-block - "> - {text} - </h3> - - <div className="max-w-[70px] text-center m-0 p-0"> - <h4 className="text-xs font-bold"> - {question_active_tasks.toLocaleString()} - </h4> - <h5 className="text-[10px]"> - Surveys use - this question - </h5> - </div> - </div> - ) -} + " + > + {text} + </h3> + + <div className="max-w-[70px] text-center m-0 p-0"> + <h4 className="text-xs font-bold"> + {question_active_tasks.toLocaleString()} + </h4> + <h5 className="text-[10px]">Surveys use this question</h5> + </div> + </div> + ); +}; const ProfilingQuestionSubmitButton = () => { - const dispatch = useAppDispatch() - - const activeQuestion = useSelector(selectActiveQuestion); - const activeQuestionValidation = useSelector(selectActiveQuestionValidation) - const bpuid = useAppSelector(state => state.app.bpuid) - - const handleOnClick = () => { - if (!activeQuestion) return - if (!activeQuestionValidation?.isComplete) return - if (!bpuid) return; - - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } - - return ( - <button - type="submit" - className="cursor-pointer text-sm font-semibold + const dispatch = useAppDispatch(); + + const activeQuestion = useSelector(selectActiveQuestion); + const activeQuestionValidation = useSelector(selectActiveQuestionValidation); + const { data: user } = useGetMeQuery(); + + const handleOnClick = () => { + if (!activeQuestion) return; + if (!activeQuestionValidation?.isComplete) return; + if (!user?.product_user_id) return; + + dispatch( + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), + ); + }; + + return ( + <button + type="submit" + className="cursor-pointer text-sm font-semibold bg-blue-700 text-white py-1 px-4 rounded hover:bg-blue-900 min-w-[148px] disabled:bg-blue-700 disabled:text-rose-300 disabled:cursor-not-allowed " - disabled={!activeQuestionValidation?.isComplete} - onClick={handleOnClick} - > - Answer Question - </button> - ) -} - + disabled={!activeQuestionValidation?.isComplete} + onClick={handleOnClick} + > + Answer Question + </button> + ); +}; export const ProfileQuestionFull = () => { - const activeQuestion = useSelector(selectActiveQuestion); - - const renderContent = () => { - if (!activeQuestion) return null; - switch (activeQuestion.question_type) { - case 'TE': - return <TextEntry /> - case 'MC': - return <MultipleChoice /> - default: - return <MultipleChoice /> - } - }; + const activeQuestion = useSelector(selectActiveQuestion); - if (!activeQuestion) return null + const renderContent = () => { + if (!activeQuestion) return null; + switch (activeQuestion.question_type) { + case "TE": + return <TextEntry />; + case "MC": + return <MultipleChoice />; + default: + return <MultipleChoice />; + } + }; - return ( - <> - {/* {answer && answer.processing && ( + if (!activeQuestion) return null; + + return ( + <> + {/* {answer && answer.processing && ( <motion.div className="bg-gray-300" initial={{ width: "0%" }} @@ -540,104 +576,111 @@ export const ProfileQuestionFull = () => { /> )} */} - {renderContent()} - </> - ) -} + {renderContent()} + </> + ); +}; const PaginationIcon: React.FC<{ - question: ProfileQuestion, idx: number, + question: ProfileQuestion; + idx: number; }> = ({ question, idx }) => { - const dispatch = useAppDispatch() + const dispatch = useAppDispatch(); - // const answers = useAppSelector(state => state.answers) - const isActive = question._metadata?.isActive ?? false - const completed = question._validation?.isComplete ?? false + // const answers = useAppSelector(state => state.answers) + const isActive = question._metadata?.isActive ?? false; + const completed = question._validation?.isComplete ?? false; - const handleSetQuestion = (evt: MouseEvent<HTMLAnchorElement>) => { - if (completed) { - evt.preventDefault() - } else { - dispatch(setActiveQuestion(question.question_id!)) - } + const handleSetQuestion = (evt: MouseEvent<HTMLAnchorElement>) => { + if (completed) { + evt.preventDefault(); + } else { + dispatch(setActiveQuestion(question.question_id!)); } - - return ( - <PaginationItem> - <PaginationLink - href="#" - title={question.question_text} - isActive={isActive} - aria-disabled={!!completed} - - onClick={handleSetQuestion} - className={clsx("cursor-pointer rounded border border-blue-300 text-blue-300 text-[10px] w-7 h-4", - { - "pointer-events-none cursor-not-allowed": completed, - "bg-blue-800 border-blue-800": isActive, - })} - > - {idx + 1} - </PaginationLink> - </PaginationItem> - ) -} + }; + + return ( + <PaginationItem> + <PaginationLink + href="#" + title={question.question_text} + isActive={isActive} + aria-disabled={!!completed} + onClick={handleSetQuestion} + className={clsx( + "cursor-pointer rounded border border-blue-300 text-blue-300 text-[10px] w-7 h-4", + { + "pointer-events-none cursor-not-allowed": completed, + "bg-blue-800 border-blue-800": isActive, + }, + )} + > + {idx + 1} + </PaginationLink> + </PaginationItem> + ); +}; const ProfilingQuestionPagination = () => { - const dispatch = useAppDispatch() - - const questions = useSelector(selectAllQuestions) - const activeQuestion = useSelector(selectActiveQuestion) - - // All the variables needed for a sliding window - const q_idx = questions.findIndex(q => q.question_id === activeQuestion!.question_id) - - const handleNextClick = () => { - dispatch(goToNextQuestion()); - }; - - const questionsWindow = ( - items: ProfileQuestion[], currentIndex: number, windowSize: number = 5 - ): ProfileQuestion[] => { - const half: number = Math.floor(windowSize / 2) - const total: number = items.length - let start: number = currentIndex - half - let end: number = currentIndex + half + 1 - - if (start < 0) { - end += Math.abs(start) - start = 0 - } - - // Adjust if window goes past the end - if (end > total) { - const overflow: number = end - total - start = Math.max(0, start - overflow) - end = total - } + const dispatch = useAppDispatch(); + + const questions = useSelector(selectAllQuestions); + const activeQuestion = useSelector(selectActiveQuestion); + + // All the variables needed for a sliding window + const q_idx = questions.findIndex( + (q) => q.question_id === activeQuestion!.question_id, + ); + + const handleNextClick = () => { + dispatch(goToNextQuestion()); + }; + + const questionsWindow = ( + items: ProfileQuestion[], + currentIndex: number, + windowSize: number = 5, + ): ProfileQuestion[] => { + const half: number = Math.floor(windowSize / 2); + const total: number = items.length; + let start: number = currentIndex - half; + let end: number = currentIndex + half + 1; + + if (start < 0) { + end += Math.abs(start); + start = 0; + } - return items.slice(start, end) + // Adjust if window goes past the end + if (end > total) { + const overflow: number = end - total; + start = Math.max(0, start - overflow); + end = total; } - return ( - <Pagination> - <PaginationContent> - { - questionsWindow(questions, q_idx).map(q => { - return <PaginationIcon - key={q.question_id} - question={q} - idx={questions.findIndex(qq => qq.question_id === q.question_id)} - /> - }) - } - - <PaginationItem> - <PaginationLink - aria-label="Go to next page" - size="default" - - className="rounded text-[10px] p-0 m-0 + return items.slice(start, end); + }; + + return ( + <Pagination> + <PaginationContent> + {questionsWindow(questions, q_idx).map((q) => { + return ( + <PaginationIcon + key={q.question_id} + question={q} + idx={questions.findIndex( + (qq) => qq.question_id === q.question_id, + )} + /> + ); + })} + + <PaginationItem> + <PaginationLink + aria-label="Go to next page" + size="default" + className="rounded text-[10px] p-0 m-0 text-blue-300 border border-blue-100 text-blue-100 hover:bg-blue-800 hover:border-blue-800 hover:text-white @@ -645,115 +688,111 @@ const ProfilingQuestionPagination = () => { h-4 w-13 sm:w-14 " - onClick={handleNextClick} - href="#" - > - <span className="block">Next</span> - <ChevronRightIcon /> - </PaginationLink> - - </PaginationItem> - </PaginationContent> - </Pagination> - ) -} + onClick={handleNextClick} + href="#" + > + <span className="block">Next</span> + <ChevronRightIcon /> + </PaginationLink> + </PaginationItem> + </PaginationContent> + </Pagination> + ); +}; const ProfilingHeader = () => { - const questions = useSelector(questionsSelectors.selectAll); + const questions = useSelector(questionsSelectors.selectAll); - return ( - <h2 className="text-xs font-mono font-bold p-0 m-0"> - {questions.length} Profiling Questions - </h2> - ) -} + return ( + <h2 className="text-xs font-mono font-bold p-0 m-0"> + {questions.length} Profiling Questions + </h2> + ); +}; const ProfilingModalBody = () => { - const activeQuestion = useSelector(selectActiveQuestion); - - if (!activeQuestion) { - return ( - <div className="p-5"> - <h3 className="text-lg font-bold">No Profiling Questions Available</h3> - <p className="my-0 text-sm text-gray-600">Please check back later.</p> - </div> - ) - } + const activeQuestion = useSelector(selectActiveQuestion); + if (!activeQuestion) { return ( - <div className="relative flex flex-col"> - {/* <ProfilingQuestionPagination /> */} - - <div className="flex-shrink-0"> - <ProfilingQuestionTitle /> - </div> - - {/* Scrollable Content */} - <div className="flex-1 p-1"> - <ProfileQuestionFull key={activeQuestion.question_id} /> - </div> - - {/* Sticky Footer */} - <div className="flex-shrink-0 bg-blue-600 p-1 flex justify-between - "> - <div className="justify-start"> - <ProfilingQuestionPagination /> - </div> - <div className="justify-end"> - <ProfilingQuestionSubmitButton /> - </div> - </div> - + <div className="p-5"> + <h3 className="text-lg font-bold">No Profiling Questions Available</h3> + <p className="my-0 text-sm text-gray-600">Please check back later.</p> + </div> + ); + } + + return ( + <div className="relative flex flex-col"> + {/* <ProfilingQuestionPagination /> */} + + <div className="flex-shrink-0"> + <ProfilingQuestionTitle /> + </div> + + {/* Scrollable Content */} + <div className="flex-1 p-1"> + <ProfileQuestionFull key={activeQuestion.question_id} /> + </div> + + {/* Sticky Footer */} + <div + className="flex-shrink-0 bg-blue-600 p-1 flex justify-between + " + > + <div className="justify-start"> + <ProfilingQuestionPagination /> </div> - ) -} + <div className="justify-end"> + <ProfilingQuestionSubmitButton /> + </div> + </div> + </div> + ); +}; const Profiling = () => { - /* This is the main UI for any User Profiling (before entering a task). It's + /* This is the main UI for any User Profiling (before entering a task). It's primary window and position should be dedictated by it's parent so this can be positioned as a window or a sidebar as needed. */ - const [showProfiling, setShowProfiling] = useState(false); + const [showProfiling, setShowProfiling] = useState(false); - const dispatch = useAppDispatch() - const bpuid = useAppSelector(state => state.app.bpuid) + const dispatch = useAppDispatch(); + const { data: user } = useGetMeQuery(); - useEffect(() => { - if (!bpuid) return; - dispatch(fetchNewQuestions(bpuid)); - }, [bpid, bpuid]) + useEffect(() => { + if (!user?.product_user_id) return; + dispatch(fetchNewQuestions(user.product_user_id)); + }, [bpid, user?.product_user_id]); - if (!bpuid) return null; + if (!user?.product_user_id) return null; - return ( - <div className={clsx( - "fixed rounded bg-blue-500 text-white shadow-lg z-60 \ + return ( + <div + className={clsx( + "fixed rounded bg-blue-500 text-white shadow-lg z-60 \ bottom-1 sm:bottom-6 \ left-1 sm:left-2", - showProfiling ? "w-[98vw] sm:w-[400px]" : "w-[220px]" - )} - > - - <div className="bg-blue-600 rounded border-white-300 + showProfiling ? "w-[98vw] sm:w-[400px]" : "w-[220px]", + )} + > + <div + className="bg-blue-600 rounded border-white-300 cursor-pointer py-1 px-4 hover:bg-blue-700 transition-colors duration-200" - onClick={() => setShowProfiling(!showProfiling)} - > - <ProfilingHeader /> - - <h4 className="text-[10px] text-center italic text-white-300 py-0"> - (Click to {showProfiling ? 'hide' : 'show'}) - </h4> - </div> - - {showProfiling && ( - <ProfilingModalBody /> - )} - - </div> - ) -} - -export { - Profiling + onClick={() => setShowProfiling(!showProfiling)} + > + <ProfilingHeader /> + + <h4 className="text-[10px] text-center italic text-white-300 py-0"> + (Click to {showProfiling ? "hide" : "show"}) + </h4> + </div> + + {showProfiling && <ProfilingModalBody />} + </div> + ); }; + +export { Profiling }; |
