From df0e07f6c50e88d98de94ac44d34b84f7f63c181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20S=C3=A1nchez?= Date: Thu, 13 Jun 2024 17:15:24 -0600 Subject: [PATCH 01/11] added loader state for user search --- app/globals.css | 20 ++++++++++++++++++++ components/ComboBoxSearch.tsx | 5 +++-- components/ProjectCard.tsx | 2 +- components/Spinner.tsx | 5 +++++ 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 components/Spinner.tsx diff --git a/app/globals.css b/app/globals.css index 011b636..2ec2cb3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -104,3 +104,23 @@ .emotion-box:has(.emotion-circle:hover) .emotion-circle:not(:hover) { transform: scale(0.9); } + +.loader { + width: 25px; + height: 25px; + border: 3px solid #bdbdbd; + border-bottom-color: #6640d5; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/components/ComboBoxSearch.tsx b/components/ComboBoxSearch.tsx index 46c4228..e78ab12 100644 --- a/components/ComboBoxSearch.tsx +++ b/components/ComboBoxSearch.tsx @@ -6,6 +6,7 @@ import { useQuery } from "@tanstack/react-query"; import { searchUsers } from "@/services/user"; import SearchBar from "./SearchBar"; import UserProfileButton from "./UserProfileButton"; +import Spinner from "./Spinner"; interface Person { id: number; @@ -60,8 +61,8 @@ const ComboBoxSearch = () => { className="absolute z-10 mt-1 max-h-52 w-full overflow-auto rounded-md border border-gray-300 bg-white p-2 shadow-lg empty:hidden" > {isLoading ? ( -
- Loading... +
+
) : people.length === 0 && query !== "" ? (
diff --git a/components/ProjectCard.tsx b/components/ProjectCard.tsx index d84e9b3..3951317 100644 --- a/components/ProjectCard.tsx +++ b/components/ProjectCard.tsx @@ -66,7 +66,7 @@ export default async function ProjectCard({ diff --git a/components/Spinner.tsx b/components/Spinner.tsx new file mode 100644 index 0000000..39630cb --- /dev/null +++ b/components/Spinner.tsx @@ -0,0 +1,5 @@ +const Spinner = () => { + return ; +}; + +export default Spinner; From 97c42ee0ae00ddb0a889cb320076f7ee28a0632c Mon Sep 17 00:00:00 2001 From: pedroalonsoms Date: Thu, 13 Jun 2024 20:22:44 -0600 Subject: [PATCH 02/11] final survey is now shown on the tasks and resource history part of the pcp --- app/(base)/pcp/page.tsx | 4 +- db/schema.ts | 3 + services/tasks-and-resources.ts | 143 ++++++++++++++++++++++++++++++-- 3 files changed, 141 insertions(+), 9 deletions(-) diff --git a/app/(base)/pcp/page.tsx b/app/(base)/pcp/page.tsx index a2da6f4..11ae0d5 100644 --- a/app/(base)/pcp/page.tsx +++ b/app/(base)/pcp/page.tsx @@ -353,7 +353,7 @@ const PCPTasksDialogContent = ({ projectId }: { projectId: number }) => { {tasksHistoryQuery.data.map((sprint) => (
{sprint.scheduledAt && ( -

{`Sprint ${formatDate(sprint.scheduledAt)}`}

+

{`${sprint.type === "SPRINT" ? "Sprint" : "Final"} Survey ${formatDate(sprint.scheduledAt)}`}

)} {sprint.processed ? ( sprint.tasks.map((task) => { @@ -649,7 +649,7 @@ const PCPResourcesDialogContent = ({ projectId }: { projectId: number }) => { {resourcesHistoryQuery.data.map((sprint) => (
{sprint.scheduledAt && ( -

{`Sprint ${formatDate(sprint.scheduledAt)}`}

+

{`${sprint.type === "SPRINT" ? "Sprint" : "Final"} Survey ${formatDate(sprint.scheduledAt)}`}

)} {sprint.processed ? ( sprint.resources.map((resource) => ( diff --git a/db/schema.ts b/db/schema.ts index d1b3f64..dbc1fd3 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -247,6 +247,9 @@ export const finalSurvey = pgTable("final_survey", { isProcessing: boolean("is_processing").default(false).notNull(), }); +export type SelectFinalSurvey = typeof finalSurvey.$inferSelect; +export type InsertFinalSurvey = typeof finalSurvey.$inferInsert; + export const finalSurveyAnswer = pgTable( "final_survey_answer", { diff --git a/services/tasks-and-resources.ts b/services/tasks-and-resources.ts index 9cceb40..c2ec7a9 100644 --- a/services/tasks-and-resources.ts +++ b/services/tasks-and-resources.ts @@ -8,6 +8,8 @@ import { pipResource, SelectPipResource, rulerSurveyAnswers, + finalSurvey, + SelectFinalSurvey, } from "@/db/schema"; import db from "@/db/drizzle"; import { eq, lte, and, desc, asc, isNotNull } from "drizzle-orm"; @@ -110,11 +112,73 @@ export async function getUserTasksHistory(projectId: number) { // TODO: inform the frontend when the survey is pending for processing - if (sprintSurveysWithTasks.length === 0) { + const finalSurveys = await db + .select({ finalSurvey: finalSurvey, task: pipTask }) + .from(finalSurvey) + .innerJoin(pipTask, eq(finalSurvey.id, pipTask.finalSurveyId)) + .where( + and( + // final surveys that belong to that project + eq(finalSurvey.projectId, projectId), + // tasks that belong to that user + eq(pipTask.userId, userId), + // final surveys that have been already scheduled (i.e. the surveys have already been launched) + lte(finalSurvey.scheduledAt, new Date()), + ), + ); + + interface SelectFinalSurveyWithTasks extends SelectFinalSurvey { + tasks: SelectPipTask[]; + } + + const finalSurveysWithTasks = finalSurveys.reduce((acc, row) => { + const { finalSurvey, task } = row; + let survey = acc.find((s) => s.id === finalSurvey.id); + + if (!survey) { + survey = { ...finalSurvey, tasks: [] }; + acc.push(survey); + } + + survey.tasks.push(task); + + return acc; + }, [] as SelectFinalSurveyWithTasks[]); + + interface SurveysWithTasks { + id: number; + type: "SPRINT" | "FINAL"; + processed: boolean | null; + scheduledAt: Date | null; + tasks: SelectPipTask[]; + } + const surveys = [] as SurveysWithTasks[]; + + for (const sprintSurveyWithTasks of sprintSurveysWithTasks) { + surveys.push({ + id: sprintSurveyWithTasks.id, + type: "SPRINT", + processed: sprintSurveyWithTasks.processed, + scheduledAt: sprintSurveyWithTasks.scheduledAt, + tasks: sprintSurveyWithTasks.tasks, + }); + } + + for (const finalSurveyWithTasks of finalSurveysWithTasks) { + surveys.push({ + id: finalSurveyWithTasks.id, + type: "FINAL", + processed: finalSurveyWithTasks.processed, + scheduledAt: finalSurveyWithTasks.scheduledAt, + tasks: finalSurveyWithTasks.tasks, + }); + } + + if (surveys.length === 0) { return "No task history available"; } - return sprintSurveysWithTasks; + return surveys; } export async function getUserResourcesForCurrentSprint(projectId: number) { @@ -201,13 +265,80 @@ export async function getUserResourcesHistory(projectId: number) { return acc; }, [] as SelectSprintSurveyWithResources[]); + const finalSurveys = await db + .select({ + resource: pipResource, + finalSurvey: finalSurvey, + }) + .from(finalSurvey) + .innerJoin(userResource, eq(finalSurvey.id, userResource.finalSurveyId)) + .innerJoin(pipResource, eq(userResource.resourceId, pipResource.id)) + .where( + and( + // final surveys that belong to that project + eq(finalSurvey.projectId, projectId), + // final surveys that have been already scheduled (i.e. the surveys have already been launched) + lte(finalSurvey.scheduledAt, new Date()), + // resources that belong to the user + eq(userResource.userId, userId), + ), + ) + .orderBy(desc(finalSurvey.scheduledAt), asc(pipResource.title)); + + interface SelectFinalSurveyWithResources extends SelectFinalSurvey { + resources: SelectPipResource[]; + } + + const finalSurveysWithResources = finalSurveys.reduce((acc, row) => { + const { finalSurvey, resource } = row; + let survey = acc.find((s) => s.id === finalSurvey.id); + + if (!survey) { + survey = { ...finalSurvey, resources: [] }; + acc.push(survey); + } + + survey.resources.push(resource); + + return acc; + }, [] as SelectFinalSurveyWithResources[]); + // TODO: inform the frontend when the survey is pending for processing - if (sprintSurveysWithResources.length === 0) { - return "No resource history available"; + interface SurveysWithResources { + id: number; + type: "SPRINT" | "FINAL"; + processed: boolean | null; + scheduledAt: Date | null; + resources: SelectPipResource[]; + } + const surveys = [] as SurveysWithResources[]; + + for (const sprintSurveyWithResources of sprintSurveysWithResources) { + surveys.push({ + id: sprintSurveyWithResources.id, + type: "SPRINT", + processed: sprintSurveyWithResources.processed, + scheduledAt: sprintSurveyWithResources.scheduledAt, + resources: sprintSurveyWithResources.resources, + }); } - return sprintSurveysWithResources; + for (const finalSurveyWithResources of finalSurveysWithResources) { + surveys.push({ + id: finalSurveyWithResources.id, + type: "FINAL", + processed: finalSurveyWithResources.processed, + scheduledAt: finalSurveyWithResources.scheduledAt, + resources: finalSurveyWithResources.resources, + }); + } + + if (surveys.length === 0) { + return "No task history available"; + } + + return surveys; } export async function updateTask({ @@ -221,8 +352,6 @@ export async function updateTask({ .update(pipTask) .set({ status: newStatus }) .where(eq(pipTask.id, taskId)); - - console.log("pipTask", taskId, newStatus); } export async function rulerResources() { From b0e86f24785756e3c49c4f7b252e7149a5bdb7c1 Mon Sep 17 00:00:00 2001 From: Eduardo de Valle Date: Thu, 13 Jun 2024 20:54:05 -0600 Subject: [PATCH 03/11] Strength and weaknesses of the user set --- services/rag.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/services/rag.ts b/services/rag.ts index 5c935a9..b4b8cc9 100644 --- a/services/rag.ts +++ b/services/rag.ts @@ -19,6 +19,7 @@ import { sprintSurveyAnswerCoworkers, sprintSurveyQuestion, userResource, + userSkill, rulerSurveyAnswers, } from "@/db/schema"; @@ -870,9 +871,29 @@ async function setUserPCP( }); } } + + // set weaknesses of the user + const weaknessesArray = Array.from(weaknessesIds); + + // Insert all values into the userSkill table + for (const weaknessId of weaknessesArray) { + await db.insert(userSkill).values({ + userId: userId, + skillId: weaknessId, + kind: "AREA_OF_OPPORTUNITY", + }); + } } - // set the strengths and weaknesses of the user + // set the strengths of the user + const strengthsArray = Array.from(strengthsIds); + for (const strengthId of strengthsArray) { + await db.insert(userSkill).values({ + userId: userId, + skillId: strengthId, + kind: "STRENGTH", + }); + } } export async function rulerAnalysis(userId: string) { From bd2a20c23e41ae71f2b631713eee34a2316b3125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20S=C3=A1nchez?= Date: Thu, 13 Jun 2024 22:01:47 -0600 Subject: [PATCH 04/11] more tests --- app/(base)/pcp/page.tsx | 36 +++++++++++++++++++++-------- app/(base)/projects/create/page.tsx | 9 +++++++- components/modals/DraggableUser.tsx | 1 + components/modals/SprintDropRow.tsx | 1 + components/modals/SprintSurvey.tsx | 5 ++-- cypress/e2e/pcp.cy.ts | 12 ++++++++++ cypress/e2e/rulerSurvey.cy.ts | 8 +++---- cypress/e2e/signin.cy.ts | 6 ++--- cypress/e2e/sprintSurvey.cy.ts | 13 +++++++++++ services/tasks-and-resources.ts | 4 +--- 10 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 cypress/e2e/pcp.cy.ts create mode 100644 cypress/e2e/sprintSurvey.cy.ts diff --git a/app/(base)/pcp/page.tsx b/app/(base)/pcp/page.tsx index fe9a7fe..4411c80 100644 --- a/app/(base)/pcp/page.tsx +++ b/app/(base)/pcp/page.tsx @@ -22,6 +22,7 @@ import ArticleIcon from "@/components/icons/ArticleIcon"; import Link from "next/link"; import ChevronRightIcon from "@/components/icons/ChevronRightIcon"; import Loader from "@/components/Loader"; +import toast from "react-hot-toast"; const statusOptions = [ { label: "Pending", color: "bg-red-500", value: "PENDING" }, @@ -250,6 +251,7 @@ const PCPTasks = ({ queryClient.invalidateQueries({ queryKey: ["tasks-history", projectId], }); + toast.success("Task status updated successfully"); const tasks = queryClient.getQueryData([ "tasks", @@ -261,6 +263,9 @@ const PCPTasks = ({ setProgressPercentage((doneTasks / totalTasks) * 100); } }, + onError: () => { + toast.error("Error updating task status"); + }, }); return ( @@ -269,6 +274,7 @@ const PCPTasks = ({

Tasks