diff --git a/.vscode/settings.json b/.vscode/settings.json index d94695f71..c41a27834 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,7 @@ "eslint.format.enable": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "always", - "source.organizeImports": "always", + "source.organizeImports": "never", "source.fixAll.markdownlint": "explicit" }, "typescript.validate.enable": true, @@ -26,5 +26,7 @@ "[markdown]": { "editor.formatOnSave": true, "editor.formatOnPaste": true - } + }, + "typescript.updateImportsOnPaste.enabled": false, + "javascript.updateImportsOnPaste.enabled": false } diff --git a/components/history/learnings.tsx b/components/history/learnings.tsx new file mode 100644 index 000000000..2876ddf78 --- /dev/null +++ b/components/history/learnings.tsx @@ -0,0 +1,27 @@ +import { type FC } from "react"; +import { AccountMapped } from "@/helpers/history/account"; + +interface Props { + account: AccountMapped; +} + +const HistoryLearnings: FC = ({ account }) => ( +
+

+ Learnings about the account and people +

+ {account.learnings.map( + (intro) => + intro.learning && ( +
+

{intro.label}

+ {intro.learning + ?.split("\n") + .map((line, idx) =>

{line}

)} +
+ ) + )} +
+); + +export default HistoryLearnings; diff --git a/components/history/notes.tsx b/components/history/notes.tsx new file mode 100644 index 000000000..6926bebf7 --- /dev/null +++ b/components/history/notes.tsx @@ -0,0 +1,20 @@ +import { type FC } from "react"; +import { type Note } from "@/helpers/history/activity"; + +interface Props { + notes?: Note[] | null; +} + +const HistoryNotes: FC = ({ notes }) => ( +
+

Project Notes

+ {notes?.map((n) => ( +
+

{n.label}

+ {n.notes?.split("\n").map((line, idx) =>

{line}

)} +
+ ))} +
+); + +export default HistoryNotes; diff --git a/components/history/people-roles.tsx b/components/history/people-roles.tsx new file mode 100644 index 000000000..e420af7e6 --- /dev/null +++ b/components/history/people-roles.tsx @@ -0,0 +1,24 @@ +import { type FC } from "react"; +import { Person } from "@/helpers/history/person"; + +interface Props { + people?: Person[] | null; +} + +const HistoryPeopleRoles: FC = ({ people }) => ( +
+

People and their roles

+ {people?.map((p) => ( +
+
{p.name}
+ {p.positions.map((pos, idx) => ( +
+ {pos.position} +
+ ))} +
+ ))} +
+); + +export default HistoryPeopleRoles; diff --git a/components/ui-elements/editors/helpers/text-generation.ts b/components/ui-elements/editors/helpers/text-generation.ts index cc8035593..5f7d0296b 100644 --- a/components/ui-elements/editors/helpers/text-generation.ts +++ b/components/ui-elements/editors/helpers/text-generation.ts @@ -34,7 +34,7 @@ const transformMentionsToText = (json: JSONContent): JSONContent => } : !json.attrs?.label ? {} - : { type: "text", text: `@${json.attrs?.label}` }; + : { type: "text", text: json.attrs?.label }; const transformTasksToText = (json: JSONContent): JSONContent => json.type !== "taskList" diff --git a/components/ui-elements/editors/helpers/transformers.ts b/components/ui-elements/editors/helpers/transformers.ts index 298db5d24..0b8536ddb 100644 --- a/components/ui-elements/editors/helpers/transformers.ts +++ b/components/ui-elements/editors/helpers/transformers.ts @@ -15,7 +15,7 @@ interface TransformNotesVersionType { forProjects: ActivityData["forProjects"]; } -const createDocument = ({ +export const createDocument = ({ formatVersion, noteBlockIds, noteBlocks, diff --git a/components/ui-elements/project-details/project-details.tsx b/components/ui-elements/project-details/project-details.tsx index 377818d1e..b5a736878 100644 --- a/components/ui-elements/project-details/project-details.tsx +++ b/components/ui-elements/project-details/project-details.tsx @@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button"; import { Context } from "@/contexts/ContextContext"; import { addDays } from "date-fns"; import { ArrowRightCircle, Loader2 } from "lucide-react"; +import Link from "next/link"; import { FC, useEffect, useState } from "react"; import ButtonGroup from "../btn-group/btn-group"; import ContextWarning from "../context-warning/context-warning"; @@ -108,6 +109,12 @@ const ProjectDetails: FC = ({ projectName={project.project} onUpdate={(name) => saveProjectName(project.id, name)} /> + + {showContext && ( diff --git a/helpers/history/account.ts b/helpers/history/account.ts new file mode 100644 index 000000000..c6638376a --- /dev/null +++ b/helpers/history/account.ts @@ -0,0 +1,99 @@ +import { union, unionBy } from "lodash"; +import { flow, identity, sortBy, filter } from "lodash/fp"; +import { getTextFromJsonContent } from "@/components/ui-elements/editors/helpers/text-generation"; +import { type Person, mapPeopleLearnings } from "./person"; +import { type Learning, getLocaleDateString, makeDate } from "./generals"; + +export type AccountMapped = { + id: string; + name: string; + shortName?: string | null; + learnings: Learning[]; +}; + +export type AccountData = { + id: string; + name: string; + shortName?: string | null; + createdAt: string; + people: { + personId: string; + }[]; + introduction?: string | null; + introductionJson: any; + learnings: { + id: string; + learnedOn?: string | null; + createdAt: string; + learning: any; + }[]; + subsidiaries: AccountData[]; +}; + +export const mapAccount = ( + a: AccountData, + people: Person[] +): AccountMapped | null => + !a + ? null + : { + id: a.id, + name: a.name, + shortName: a.shortName, + learnings: mapLearning(a)(people), + }; + +const mapLearning = (a: AccountData) => + flow( + identity, + mapPeopleLearnings, + (l) => unionBy(mapIntroduction(a), mapAccountLearning(a), l, "learnedOn"), + filter((l) => !!l.learning), + sortBy((a) => a.learnedOn.getTime()) + ); + +const mapIntroduction = (a: AccountData): Learning[] => + a && + Boolean( + a.introduction || + getTextFromJsonContent(JSON.parse(a.introductionJson as any)) + ) + ? union( + [ + { + id: a.id, + label: getLocaleDateString(a.createdAt), + learnedOn: makeDate(a.createdAt), + learning: `${a.name}${!a.shortName ? "" : ` (${a.shortName})`}\n${ + !a.introductionJson + ? a.introduction + : getTextFromJsonContent(JSON.parse(a.introductionJson as any)) + }`, + }, + ], + a.subsidiaries?.flatMap(mapIntroduction) + ) + : []; + +const mapAccountLearning = (a: AccountData): Learning[] => + !a?.learnings + ? [] + : a.learnings.reduce( + (prev, curr) => + unionBy( + prev, + [ + { + id: curr.id, + label: getLocaleDateString(curr.createdAt, curr.learnedOn), + learnedOn: makeDate(curr.createdAt, curr.learnedOn), + learning: !curr.learning + ? "" + : getTextFromJsonContent(JSON.parse(curr.learning as any)), + }, + ], + a.subsidiaries?.flatMap(mapAccountLearning), + "id" + ), + [] as Learning[] + ); diff --git a/helpers/history/activity.ts b/helpers/history/activity.ts new file mode 100644 index 000000000..dc42824cd --- /dev/null +++ b/helpers/history/activity.ts @@ -0,0 +1,33 @@ +import { flow, get, map, identity, replace, sortBy } from "lodash/fp"; +import { Project } from "./project"; +import { makeDate, getLocaleDateString } from "./generals"; +import { getTextFromJsonContent } from "@/components/ui-elements/editors/helpers/text-generation"; +import { createDocument } from "@/components/ui-elements/editors/helpers/transformers"; + +export type Note = { + id: string; + date: Date; + label: string; + notes: string; +}; + +export const mapNotes = flow( + identity, + get("activities"), + map("activity"), + map( + (a): Note => ({ + id: a.id, + date: makeDate(a.createdAt, a.finishedOn), + label: `${!a.forMeeting ? `On ${getLocaleDateString(a.createdAt, a.finishedOn)}` : `Meeting: ${a.forMeeting.topic} (${getLocaleDateString(a.createdAt, a.finishedOn)})`}`, + notes: flow( + identity, + createDocument, + getTextFromJsonContent, + replace(/\[\]\n\n/g, "[] "), + replace(/\[x\]\n\n/g, "[x] ") + )(a), + }) + ), + sortBy((a) => a.date.getTime()) +); diff --git a/helpers/history/generals.ts b/helpers/history/generals.ts new file mode 100644 index 000000000..e12c82760 --- /dev/null +++ b/helpers/history/generals.ts @@ -0,0 +1,14 @@ +import { toLocaleDateString } from "@/helpers/functional"; +import { flow } from "lodash/fp"; + +export const makeDate = (fallback: string, date?: string | null) => + new Date(date || fallback); + +export const getLocaleDateString = flow(makeDate, toLocaleDateString); + +export type Learning = { + id: string; + label: string; + learnedOn: Date; + learning: string; +}; diff --git a/helpers/history/person.ts b/helpers/history/person.ts new file mode 100644 index 000000000..8f45e4f27 --- /dev/null +++ b/helpers/history/person.ts @@ -0,0 +1,147 @@ +import { SelectionSet } from "aws-amplify/data"; +import { Schema } from "@/amplify/data/resource"; +import { getTextFromJsonContent } from "@/components/ui-elements/editors/helpers/text-generation"; +import { client } from "@/pages/projects/[id]/history/index"; +import { union } from "lodash"; +import { sortBy, identity, map, flow } from "lodash/fp"; +import { AccountData } from "./account"; +import { getLocaleDateString, makeDate, type Learning } from "./generals"; +import { type Project } from "./project"; + +export type Person = { + id: string; + name: string; + learnings: Learning[]; + positions: { + accountId: string; + position: string; + }[]; +}; + +export const getPeople = async (peopleIds: string[]) => { + const people = await Promise.all(peopleIds.map(mapPersonId)); + return people.filter((p) => p !== null); +}; + +export const mapPeopleIds = (a: AccountData): string[] => + !a.people + ? [] + : a.people.reduce((prev, curr) => { + const subs = a.subsidiaries?.flatMap(mapPeopleIds); + return union(prev, [curr.personId], subs); + }, [] as string[]); + +export const mapPeopleLearnings = (people: Person[]): Learning[] => + people + .filter((p) => p?.learnings?.length && p.learnings.length > 0) + .flatMap((p) => p.learnings); + +const mapPersonId = async (id: string) => { + const person = await getPerson(id); + if (!person) return null; + return person; +}; + +const getPerson = async (personId: string): Promise => { + const { data, errors } = await client.models.Person.get( + { id: personId }, + { selectionSet } + ); + if (errors || !data) return null; + return mapPerson(data); +}; + +const mapLearning = + (name: string) => + ({ + id, + learnedOn, + createdAt, + learning, + }: PersonData["learnings"][number]): Learning => ({ + id: id, + label: `${name} (${getLocaleDateString(createdAt, learnedOn)})`, + learnedOn: makeDate(createdAt, learnedOn), + learning: getTextFromJsonContent(JSON.parse(learning as any)), + }); + +const mapPerson = ({ id, name, learnings, accounts }: PersonData): Person => ({ + id, + name, + learnings: flow( + identity, + map(mapLearning(name)), + sortBy((l) => l.learnedOn.getTime()) + )(learnings), + positions: flow( + identity, + map(mapPosition), + sortPositions + )(accounts), +}); + +type PersonPosition = { + accountId: string; + position?: string | null; + startDate?: string | null; + endDate?: string | null; +}; + +const sortPositions = (positions: PersonPosition[]) => + positions.sort(comparePositions); + +const diffDates = (a: string, b: string) => + new Date(b).getTime() - new Date(a).getTime(); + +const comparePositions = (a: PersonPosition, b: PersonPosition) => + !b.endDate + ? !a.endDate + ? !b.startDate + ? 0 + : !a.startDate + ? 0 + : diffDates(b.startDate, a.startDate) + : !b.startDate + ? 0 + : diffDates(b.startDate, a.endDate) + : !a.endDate + ? !a.startDate + ? 0 + : diffDates(b.endDate, a.startDate) + : diffDates(b.endDate, a.endDate); + +const mapPosition = ({ + startDate, + endDate, + position, + account, +}: PersonData["accounts"][number]) => { + const timeTxt = + startDate && endDate + ? `${getLocaleDateString(startDate)} - ${getLocaleDateString(endDate)}: ` + : startDate + ? `Since ${getLocaleDateString(startDate)}: ` + : endDate + ? `Until ${getLocaleDateString(endDate)}: ` + : ""; + return { + accountId: account.id, + position: !position ? "" : `${timeTxt}${position}`, + }; +}; + +const selectionSet = [ + "id", + "name", + "learnings.id", + "learnings.learnedOn", + "learnings.createdAt", + "learnings.learning", + "accounts.startDate", + "accounts.endDate", + "accounts.position", + "accounts.account.id", + "accounts.account.name", +] as const; + +type PersonData = SelectionSet; diff --git a/helpers/history/project.ts b/helpers/history/project.ts new file mode 100644 index 000000000..9f1e7861a --- /dev/null +++ b/helpers/history/project.ts @@ -0,0 +1,88 @@ +import { SelectionSet, generateClient } from "aws-amplify/data"; +import { Schema } from "@/amplify/data/resource"; +import { Dispatch, SetStateAction } from "react"; +import { client } from "@/pages/projects/[id]/history/index"; + +export type Project = SelectionSet< + Schema["Projects"]["type"], + typeof selectionSet +>; + +export const getProject = async ( + projectId: string, + setProjectName: Dispatch> +): Promise => { + const { data, errors } = await client.models.Projects.get( + { id: projectId }, + { selectionSet } + ); + if (errors || !data) return null; + setProjectName(data.project); + return data; +}; + +const selectionSet = [ + "project", + "accounts.account.id", + "accounts.account.name", + "accounts.account.shortName", + "accounts.account.createdAt", + "accounts.account.people.personId", + "accounts.account.introduction", + "accounts.account.introductionJson", + "accounts.account.learnings.id", + "accounts.account.learnings.learnedOn", + "accounts.account.learnings.createdAt", + "accounts.account.learnings.learning", + "accounts.account.subsidiaries.id", + "accounts.account.subsidiaries.name", + "accounts.account.subsidiaries.shortName", + "accounts.account.subsidiaries.createdAt", + "accounts.account.subsidiaries.people.personId", + "accounts.account.subsidiaries.introduction", + "accounts.account.subsidiaries.introductionJson", + "accounts.account.subsidiaries.learnings.id", + "accounts.account.subsidiaries.learnings.learnedOn", + "accounts.account.subsidiaries.learnings.createdAt", + "accounts.account.subsidiaries.learnings.learning", + "accounts.account.subsidiaries.subsidiaries.id", + "accounts.account.subsidiaries.subsidiaries.name", + "accounts.account.subsidiaries.subsidiaries.shortName", + "accounts.account.subsidiaries.subsidiaries.createdAt", + "accounts.account.subsidiaries.subsidiaries.people.personId", + "accounts.account.subsidiaries.subsidiaries.introduction", + "accounts.account.subsidiaries.subsidiaries.introductionJson", + "accounts.account.subsidiaries.subsidiaries.learnings.id", + "accounts.account.subsidiaries.subsidiaries.learnings.learnedOn", + "accounts.account.subsidiaries.subsidiaries.learnings.createdAt", + "accounts.account.subsidiaries.subsidiaries.learnings.learning", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.id", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.name", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.shortName", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.createdAt", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.people.personId", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.introduction", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.introductionJson", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.learnings.id", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.learnings.learnedOn", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.learnings.createdAt", + // "accounts.account.subsidiaries.subsidiaries.subsidiaries.learnings.learning", + "activities.activity.id", + "activities.activity.finishedOn", + "activities.activity.createdAt", + "activities.activity.forMeeting.topic", + "activities.activity.forMeeting.participants.person.id", + "activities.activity.forMeeting.participants.person.name", + "activities.activity.formatVersion", + "activities.activity.noteBlockIds", + "activities.activity.noteBlocks.id", + "activities.activity.noteBlocks.content", + "activities.activity.noteBlocks.type", + "activities.activity.noteBlocks.todo.id", + "activities.activity.noteBlocks.todo.todo", + "activities.activity.noteBlocks.todo.status", + "activities.activity.noteBlocks.people.id", + "activities.activity.noteBlocks.people.personId", + "activities.activity.notes", + "activities.activity.notesJson", +] as const; diff --git a/pages/projects/[id]/history/index.tsx b/pages/projects/[id]/history/index.tsx new file mode 100644 index 000000000..cb7330bdc --- /dev/null +++ b/pages/projects/[id]/history/index.tsx @@ -0,0 +1,111 @@ +import { generateClient } from "aws-amplify/data"; +import { generateClient as generateApi } from "aws-amplify/api"; +import { flow, identity, get, map } from "lodash/fp"; +import { useRouter } from "next/router"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { Schema } from "@/amplify/data/resource"; +import HistoryLearnings from "@/components/history/learnings"; +import HistoryPeopleRoles from "@/components/history/people-roles"; +import HistoryNotes from "@/components/history/notes"; +import MainLayout from "@/components/layouts/MainLayout"; +import { mapAccount, type AccountMapped } from "@/helpers/history/account"; +import { mapNotes, type Note } from "@/helpers/history/activity"; +import { getPeople, type Person, mapPeopleIds } from "@/helpers/history/person"; +import { getProject, type Project } from "@/helpers/history/project"; +import { Button } from "@/components/ui/button"; + +export const client = generateClient(); +const api = generateApi({ authMode: "userPool" }); + +const loadKnowledge = async ( + projectId: string, + setProjectName: Dispatch>, + setAccounts: Dispatch>, + setPeople: Dispatch>, + setNotes: Dispatch> +) => { + const project = await getProject(projectId, setProjectName); + if (!project) return; + const peopleIds = project.accounts?.flatMap((a) => mapPeopleIds(a?.account)); + const people = await getPeople(peopleIds); + setPeople(people); + flow(identity, mapNotes, setNotes)(project); + flow( + identity, + get("accounts"), + map((a) => mapAccount(a?.account, people)), + setAccounts + )(project); +}; + +const ProjectHistoryPage = () => { + const router = useRouter(); + const { id } = router.query; + const projectId = Array.isArray(id) ? id[0] : id; + const [accounts, setAccounts] = useState(null); + const [projectName, setProjectName] = useState("Loading…"); + const [people, setPeople] = useState(null); + const [notes, setNotes] = useState(null); + + useEffect(() => { + if (!projectId) return; + loadKnowledge(projectId, setProjectName, setAccounts, setPeople, setNotes); + }, [projectId]); + + const generateContent = async () => { + console.log("generateContent"); + const content = [ + ...(accounts?.flatMap((a) => [ + `Account: ${a.name}`, + "", + ...[ + "Learnings about the account and people", + "", + ...a.learnings.flatMap((intro) => + !intro.learning ? [] : [intro.label, intro.learning] + ), + "", + ], + "", + "", + "People and their roles", + "", + ...(people?.flatMap((p) => [ + p.name, + ...p.positions.map((pos) => pos.position), + ]) ?? []), + "", + "", + "Project Notes", + "", + ...(notes?.flatMap((n) => [n.label, n.notes]) ?? []), + "", + ]) ?? []), + ].join("\n"); + const { data, errors } = await api.generations.rewriteProjectNotes({ + content, + }); + console.log({ data, errors }); + }; + + return ( + +
+ + {accounts?.map((a) => ( +
+

Account: {a.name}

+ + + + + + +
+ ))} +
+
+ ); +}; + +export default ProjectHistoryPage; diff --git a/pages/projects/[id].tsx b/pages/projects/[id]/index.tsx similarity index 100% rename from pages/projects/[id].tsx rename to pages/projects/[id]/index.tsx