Skip to content

Commit

Permalink
feat: group projects by accounts in weekly planning and work context
Browse files Browse the repository at this point in the history
  • Loading branch information
Carsten Koch committed Oct 14, 2024
1 parent 09a654a commit ca03fc6
Show file tree
Hide file tree
Showing 11 changed files with 542 additions and 171 deletions.
29 changes: 29 additions & 0 deletions components/planning/PlanWeekContextNotWork.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import MakeProjectDecision from "@/components/planning/MakeProjectDecision";
import { usePlanningProjectFilter } from "@/components/planning/usePlanningProjectFilter";
import { Accordion } from "@/components/ui/accordion";
import { useWeekPlanContext } from "./useWeekPlanContext";

const PlanWeekContextNotWork = () => {
const { weekPlan, startDate } = useWeekPlanContext();
const { projects, saveProjectDates } = usePlanningProjectFilter();

return (
weekPlan && (
<Accordion type="single" collapsible>
{projects.map((project) => (
<MakeProjectDecision
startDate={startDate}
key={project.id}
isInFocus={weekPlan.projectIds.some((id) => id === project.id)}
project={project}
saveOnHoldDate={(onHoldTill) =>
saveProjectDates({ projectId: project.id, onHoldTill })
}
/>
))}
</Accordion>
)
);
};

export default PlanWeekContextNotWork;
59 changes: 59 additions & 0 deletions components/planning/PlanWeekContextWork.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { make2YearsRevenueText } from "@/helpers/projects";
import { Loader2 } from "lucide-react";
import ApiLoadingError from "../layouts/ApiLoadingError";
import DefaultAccordionItem from "../ui-elements/accordion/DefaultAccordionItem";
import { Accordion } from "../ui/accordion";
import MakeProjectDecision from "./MakeProjectDecision";
import {
usePlanAccountProjects,
withPlanAccountProjects,
} from "./usePlanAccountProjects";
import { useWeekPlanContext } from "./useWeekPlanContext";

const PlanWeekContextWork = () => {
const { weekPlan, startDate } = useWeekPlanContext();
const { accountsProjects, loadingAccounts, errorAccounts, saveProjectDates } =
usePlanAccountProjects();

return (
<div className="space-y-6">
<ApiLoadingError error={errorAccounts} title="Error loading accounts" />

{loadingAccounts && (
<Loader2 className="mt-2 ml-2 h-6 w-6 animate-spin" />
)}

<Accordion type="single" collapsible>
{accountsProjects?.map(({ id, name, pipeline, projects }) => (
<DefaultAccordionItem
key={id}
triggerTitle={name}
triggerSubTitle={[
`${projects.length} projects`,
make2YearsRevenueText(pipeline),
]}
value={id}
>
<Accordion type="single" collapsible>
{projects.map((project) => (
<MakeProjectDecision
startDate={startDate}
key={project.id}
isInFocus={weekPlan?.projectIds.some(
(id) => id === project.id
)}
project={project}
saveOnHoldDate={(onHoldTill) =>
saveProjectDates({ projectId: project.id, onHoldTill })
}
/>
))}
</Accordion>
</DefaultAccordionItem>
))}
</Accordion>
</div>
);
};

export default withPlanAccountProjects(PlanWeekContextWork);
27 changes: 27 additions & 0 deletions components/planning/PlanWeekFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ButtonGroup from "@/components/ui-elements/btn-group/btn-group";
import { Label } from "@/components/ui/label";
import { projectFilters, ProjectFilters } from "@/helpers/planning";
import { usePlanningProjectFilter } from "./usePlanningProjectFilter";

const PlanWeekFilter = () => {
const { projectFilter, setProjectFilter } = usePlanningProjectFilter();

return (
<div className="space-y-2">
<Label htmlFor="project-filter" className="mx-2 font-semibold">
Filter projects
</Label>
<ButtonGroup
elementId="project-filter"
values={["Open", "In Focus", "On Hold"]}
selectedValue={projectFilter}
onSelect={(val: string) =>
projectFilters.includes(val as ProjectFilters) &&
setProjectFilter(val as ProjectFilters)
}
/>
</div>
);
};

export default PlanWeekFilter;
47 changes: 47 additions & 0 deletions components/planning/PlanWeekForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import DateSelector from "@/components/ui-elements/selectors/date-selector";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { CalendarCheck, Loader2, Play } from "lucide-react";
import { useWeekPlanContext } from "./useWeekPlanContext";

const PlanWeekForm = () => {
const {
weekPlan,
startDate,
setStartDate,
isLoading,
confirmProjectSelection,
createWeekPlan,
} = useWeekPlanContext();

return (
<div className="space-y-2">
<Label htmlFor="week-start-date" className="font-semibold">
Start date of the week
</Label>
<DateSelector
disabled={!!weekPlan}
elementId="week-start-date"
date={startDate}
setDate={setStartDate}
/>
{isLoading ? (
<Loader2 className="mt-2 ml-2 h-6 w-6 animate-spin" />
) : weekPlan ? (
<Button onClick={confirmProjectSelection}>
<div className="flex flex-row gap-2 items-center">
<CalendarCheck className="w-4 h-4" /> Confirm Project Selection
</div>
</Button>
) : (
<Button onClick={() => createWeekPlan(startDate)}>
<div className="flex flex-row gap-2 items-center">
<Play className="w-4 h-4" /> Start Week Planning
</div>
</Button>
)}
</div>
);
};

export default PlanWeekForm;
28 changes: 28 additions & 0 deletions components/planning/PlanWeekStatistics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { usePlanningProjectFilter } from "./usePlanningProjectFilter";
import { useWeekPlanContext } from "./useWeekPlanContext";

const PlanWeekStatistics = () => {
const { weekPlan } = useWeekPlanContext();
const { openProjectsCount, onholdProjectsCount, focusProjectsCount } =
usePlanningProjectFilter();

return (
<div className="mx-2 md:mx-4 my-8 font-semibold text-sm text-muted-foreground md:text-center">
{!weekPlan ? (
"Start Week Planning to review a list of projects for the current context."
) : (
<div>
<div>
Review each project and decide if you can make progress here during
the next week.
</div>
<div>Projects to be reviewed: {openProjectsCount}</div>
<div>Projects on hold: {onholdProjectsCount}</div>
<div>Projects in focus: {focusProjectsCount}</div>
</div>
)}
</div>
);
};

export default PlanWeekStatistics;
88 changes: 88 additions & 0 deletions components/planning/usePlanAccountProjects.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useAccountsContext } from "@/api/ContextAccounts";
import {
AccountProjects,
mapAccountOrder,
mapAccountProjects,
} from "@/helpers/planning";
import { filter, flow, map, sortBy } from "lodash/fp";
import {
ComponentType,
createContext,
FC,
useContext,
useEffect,
useState,
} from "react";
import { usePlanningProjectFilter } from "./usePlanningProjectFilter";

interface PlanAccountProjectsType {
accountsProjects: AccountProjects[];
loadingAccounts: boolean;
errorAccounts: any;
saveProjectDates: (props: {
projectId: string;
dueDate?: Date;
doneOn?: Date;
onHoldTill?: Date | null;
}) => Promise<string | undefined>;
}

const PlanAccountProjects = createContext<PlanAccountProjectsType | null>(null);

export const usePlanAccountProjects = () => {
const searchContext = useContext(PlanAccountProjects);
if (!searchContext)
throw new Error(
"usePlanAccountProjects must be used within PlanAccountProjectsProvider"
);
return searchContext;
};

interface PlanAccountProjectsProviderProps {
children: React.ReactNode;
}

export const PlanAccountProjectsProvider: FC<
PlanAccountProjectsProviderProps
> = ({ children }) => {
const { accounts, loadingAccounts, errorAccounts } = useAccountsContext();
const { projects, saveProjectDates } = usePlanningProjectFilter();
const [accountsProjects, setAccountsProjects] = useState<AccountProjects[]>(
[]
);

useEffect(() => {
flow(
map(mapAccountProjects(projects)),
filter(({ projects }) => projects.length > 0),
map(mapAccountOrder),
sortBy((a) => -a.order),
setAccountsProjects
)(accounts);
}, [accounts, projects]);

return (
<PlanAccountProjects.Provider
value={{
accountsProjects,
loadingAccounts,
errorAccounts,
saveProjectDates,
}}
>
{children}
</PlanAccountProjects.Provider>
);
};

export function withPlanAccountProjects<Props extends object>(
Component: ComponentType<Props>
) {
return function WrappedProvider(componentProps: Props) {
return (
<PlanAccountProjectsProvider>
<Component {...componentProps} />
</PlanAccountProjectsProvider>
);
};
}
102 changes: 102 additions & 0 deletions components/planning/usePlanningProjectFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useAccountsContext } from "@/api/ContextAccounts";
import { Project, useProjectsContext } from "@/api/ContextProjects";
import {
filterAndSortProjectsForWeeklyPlanning,
ProjectFilters,
setProjectsFilterCount,
} from "@/helpers/planning";
import { flow } from "lodash/fp";
import { createContext, FC, useContext, useEffect, useState } from "react";
import { useWeekPlanContext } from "./useWeekPlanContext";

interface PlanningProjectFilterType {
projects: Project[];
projectFilter: ProjectFilters;
setProjectFilter: (filt: ProjectFilters) => void;
openProjectsCount: number;
focusProjectsCount: number;
onholdProjectsCount: number;
saveProjectDates: (props: {
projectId: string;
dueDate?: Date;
doneOn?: Date;
onHoldTill?: Date | null;
}) => Promise<string | undefined>;
}

const PlanningProjectFilter = createContext<PlanningProjectFilterType | null>(
null
);

export const usePlanningProjectFilter = () => {
const searchContext = useContext(PlanningProjectFilter);
if (!searchContext)
throw new Error(
"usePlanningProjectFilter must be used within PlanningProjectFilterProvider"
);
return searchContext;
};

interface PlanningProjectFilterProviderProps {
children: React.ReactNode;
}

export const PlanningProjectFilterProvider: FC<
PlanningProjectFilterProviderProps
> = ({ children }) => {
const { projects, saveProjectDates } = useProjectsContext();
const { accounts } = useAccountsContext();
const { weekPlan, startDate } = useWeekPlanContext();
const [projectFilter, setProjectFilter] = useState<ProjectFilters>("Open");
const [filteredAndSortedProjects, setFilteredAndSortedProjects] = useState(
filterAndSortProjectsForWeeklyPlanning(
accounts,
startDate,
weekPlan,
projectFilter
)(projects)
);
const [openProjectsCount, setOpenProjectsCount] = useState(0);
const [focusProjectsCount, setFocusProjectsCount] = useState(0);
const [onholdProjectsCount, setOnholdProjectsCount] = useState(0);

useEffect(() => {
flow(
filterAndSortProjectsForWeeklyPlanning(
accounts,
startDate,
weekPlan,
projectFilter
),
setFilteredAndSortedProjects
)(projects);
}, [accounts, projectFilter, projects, startDate, weekPlan]);

useEffect(() => {
setProjectsFilterCount(
projects,
accounts,
startDate,
weekPlan,
setOpenProjectsCount,
setFocusProjectsCount,
setOnholdProjectsCount
);
}, [accounts, projects, startDate, weekPlan]);

return (
<PlanningProjectFilter.Provider
value={{
projects: filteredAndSortedProjects,
projectFilter,
setProjectFilter,
openProjectsCount,
onholdProjectsCount,
focusProjectsCount,
saveProjectDates,
}}
>
{children}
</PlanningProjectFilter.Provider>
);
};
Loading

0 comments on commit ca03fc6

Please sign in to comment.