aboutsummaryrefslogtreecommitdiff
path: root/jb-ui/src/components/Profiling.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'jb-ui/src/components/Profiling.tsx')
-rw-r--r--jb-ui/src/components/Profiling.tsx1313
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 };