diff --git a/src/core/jupiter/core/domain/inbox_tasks/inbox_task.py b/src/core/jupiter/core/domain/inbox_tasks/inbox_task.py index d1daca29..da22f9de 100644 --- a/src/core/jupiter/core/domain/inbox_tasks/inbox_task.py +++ b/src/core/jupiter/core/domain/inbox_tasks/inbox_task.py @@ -1111,6 +1111,7 @@ async def find_completed_in_range( allow_archived: bool, filter_start_completed_date: ADate, filter_end_completed_date: ADate, + filter_include_sources: Iterable[InboxTaskSource], filter_exclude_ref_ids: Iterable[EntityId] | None = None, ) -> list[InboxTask]: """Find all completed inbox tasks in a time range.""" diff --git a/src/core/jupiter/core/repository/sqlite/domain/inbox_tasks.py b/src/core/jupiter/core/repository/sqlite/domain/inbox_tasks.py index 401fda14..b92969c7 100644 --- a/src/core/jupiter/core/repository/sqlite/domain/inbox_tasks.py +++ b/src/core/jupiter/core/repository/sqlite/domain/inbox_tasks.py @@ -136,6 +136,7 @@ async def find_completed_in_range( allow_archived: bool, filter_start_completed_date: ADate, filter_end_completed_date: ADate, + filter_include_sources: Iterable[InboxTaskSource], filter_exclude_ref_ids: Iterable[EntityId] | None = None, ) -> list[InboxTask]: """find all completed inbox tasks in a time range.""" @@ -156,6 +157,7 @@ async def find_completed_in_range( s.value for s in InboxTaskStatus.all_completed_statuses() ) ) + .where(self._table.c.source.in_(s.value for s in filter_include_sources)) .where(self._table.c.completed_time.is_not(None)) .where(self._table.c.completed_time >= start_completed_time.the_ts) .where(self._table.c.completed_time <= end_completed_time.the_ts) diff --git a/src/core/jupiter/core/use_cases/time_plans/activity/load.py b/src/core/jupiter/core/use_cases/time_plans/activity/load.py index fe37af00..990bb385 100644 --- a/src/core/jupiter/core/use_cases/time_plans/activity/load.py +++ b/src/core/jupiter/core/use_cases/time_plans/activity/load.py @@ -60,6 +60,7 @@ async def _perform_transactional_read( TimePlanActivity.target_inbox_task, TimePlanActivity.target_big_plan, allow_archived=args.allow_archived, + allow_subentity_archived=args.allow_archived, ) if not workspace.is_feature_available(WorkspaceFeature.BIG_PLANS): diff --git a/src/core/jupiter/core/use_cases/time_plans/load.py b/src/core/jupiter/core/use_cases/time_plans/load.py index 52f81118..a6f3a0d1 100644 --- a/src/core/jupiter/core/use_cases/time_plans/load.py +++ b/src/core/jupiter/core/use_cases/time_plans/load.py @@ -108,6 +108,15 @@ async def _perform_transactional_read( completed_nontarget_inbox_tasks = None if args.include_completed_nontarget and target_inbox_tasks is not None: + + # The rule here should be: + # If this is a inbox task or big plan include it always + # If this is a generated one, then: + # If the recurring_task_period is strictly higher than the time plan is we include it + # If the recurring_task_period is equal or lower than the time plan one we skip it + # expressed as: (it.source in (user, big-plan)) or (it.period in (*all_higher_periods) + # But this is hard to express cause inbox_tasks don't yet remember the period + # of their source entity. Inference from the timeline is hard in SQL, etc. completed_nontarget_inbox_tasks = await uow.get( InboxTaskRepository ).find_completed_in_range( @@ -115,6 +124,7 @@ async def _perform_transactional_read( allow_archived=True, filter_start_completed_date=schedule.first_day, filter_end_completed_date=schedule.end_day, + filter_include_sources=[InboxTaskSource.USER, InboxTaskSource.BIG_PLAN], filter_exclude_ref_ids=[it.ref_id for it in target_inbox_tasks], ) @@ -153,9 +163,9 @@ async def _perform_transactional_read( target_inbox_tasks_by_ref_id = { it.ref_id: it for it in cast(list[InboxTask], target_inbox_tasks) } - target_big_plans_by_ref_id = { - bp.ref_id: bp for bp in cast(list[BigPlan], target_big_plans) - } + target_big_plans_by_ref_id = ( + {bp.ref_id: bp for bp in target_big_plans} if target_big_plans else {} + ) activities_by_big_plan_ref_id: defaultdict[ EntityId, list[EntityId] ] = defaultdict(list) @@ -169,7 +179,6 @@ async def _perform_transactional_read( if activity.kind == TimePlanActivityKind.FINISH: activity_doneness[activity.ref_id] = inbox_task.is_completed elif activity.kind == TimePlanActivityKind.MAKE_PROGRESS: - print(time_plan.start_date, time_plan.end_date, inbox_task) modified_in_time_plan = ( inbox_task.is_working_or_more and time_plan.start_date.to_timestamp_at_start_of_day() diff --git a/src/webui/app/components/infra/layout/nesting-aware-block.tsx b/src/webui/app/components/infra/layout/nesting-aware-block.tsx index 8c3f96a7..16b12fe6 100644 --- a/src/webui/app/components/infra/layout/nesting-aware-block.tsx +++ b/src/webui/app/components/infra/layout/nesting-aware-block.tsx @@ -1,5 +1,4 @@ import { Stack } from "@mui/system"; -import { Form } from "@remix-run/react"; import type { PropsWithChildren } from "react"; import { useBigScreen } from "~/rendering/use-big-screen"; @@ -22,14 +21,12 @@ export function NestingAwareBlock( } return ( -
- - {props.children} - -
+ + {props.children} + ); } diff --git a/src/webui/app/logic/domain/time-plan-activity.ts b/src/webui/app/logic/domain/time-plan-activity.ts index 24700928..d55b2f62 100644 --- a/src/webui/app/logic/domain/time-plan-activity.ts +++ b/src/webui/app/logic/domain/time-plan-activity.ts @@ -4,8 +4,6 @@ import { type InboxTask, type TimePlanActivity, } from "@jupiter/webapi-client"; -import { isCompleted as isBigPlanCompleted } from "./big-plan-status"; -import { isCompleted as isInboxTaskCompleted } from "./inbox-task-status"; import { compareTimePlanActivityFeasability } from "./time-plan-activity-feasability"; import { compareTimePlanActivityKind } from "./time-plan-activity-kind"; @@ -17,22 +15,24 @@ const TIME_PLAN_ACTIVITY_TARGET_MAP = { export function filterActivitiesByTargetStatus( timePlanActivities: TimePlanActivity[], targetInboxTasks: Map, - targetBigPlans: Map + targetBigPlans: Map, + activityDoneness: Record ): TimePlanActivity[] { return timePlanActivities.filter((activity) => { - if (activity.target === TimePlanActivityTarget.INBOX_TASK) { - const inboxTask = targetInboxTasks.get(activity.target_ref_id); - if (!inboxTask) { - return false; - } - return !isInboxTaskCompleted(inboxTask.status); - } else { - const bigPlan = targetBigPlans.get(activity.target_ref_id); - if (!bigPlan) { - return false; - } - return !isBigPlanCompleted(bigPlan.status); + if (activityDoneness[activity.ref_id]) { + return false; } + + switch (activity.target) { + case TimePlanActivityTarget.INBOX_TASK: + const inboxTask = targetInboxTasks.get(activity.target_ref_id)!; + return !inboxTask.archived; + case TimePlanActivityTarget.BIG_PLAN: + const bigPlan = targetBigPlans.get(activity.target_ref_id)!; + return !bigPlan.archived; + } + + throw new Error("This should not happen"); }); } diff --git a/src/webui/app/routes/workspace/time-plans/$id.tsx b/src/webui/app/routes/workspace/time-plans/$id.tsx index c802dc17..4ba47862 100644 --- a/src/webui/app/routes/workspace/time-plans/$id.tsx +++ b/src/webui/app/routes/workspace/time-plans/$id.tsx @@ -1,8 +1,9 @@ import type { BigPlan, InboxTask, - ProjectSummary, TimePlan, + TimePlanActivity, + Workspace, } from "@jupiter/webapi-client"; import { ApiError, @@ -10,19 +11,26 @@ import { TimePlanActivityTarget, WorkspaceFeature, } from "@jupiter/webapi-client"; +import FlareIcon from "@mui/icons-material/Flare"; +import ViewListIcon from "@mui/icons-material/ViewList"; import { + Box, Button, + ButtonGroup, + Divider, FormControl, InputLabel, MenuItem, OutlinedInput, Select, Stack, + Typography, } from "@mui/material"; import type { ActionArgs, LoaderArgs } from "@remix-run/node"; import { json, redirect, Response } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; import { + Form, Link, Outlet, useActionData, @@ -31,7 +39,7 @@ import { } from "@remix-run/react"; import { AnimatePresence } from "framer-motion"; import { ReasonPhrases, StatusCodes } from "http-status-codes"; -import { useContext } from "react"; +import { useContext, useEffect, useState } from "react"; import { z } from "zod"; import { parseForm, parseParams } from "zodix"; import { getLoggedInApiClient } from "~/api-clients"; @@ -52,6 +60,10 @@ import { validationErrorToUIErrorInfo, } from "~/logic/action-result"; import { periodName } from "~/logic/domain/period"; +import { + computeProjectHierarchicalNameFromRoot, + sortProjectsByTreeOrder, +} from "~/logic/domain/project"; import { sortTimePlansNaturally } from "~/logic/domain/time-plan"; import { sortTimePlanActivitiesNaturally } from "~/logic/domain/time-plan-activity"; import { isWorkspaceFeatureAvailable } from "~/logic/domain/workspace"; @@ -62,8 +74,14 @@ import { useBranchNeedsToShowLeaf, } from "~/rendering/use-nested-entities"; import { getSession } from "~/sessions"; +import type { TopLevelInfo } from "~/top-level-context"; import { TopLevelInfoContext } from "~/top-level-context"; +enum View { + MERGED = "merged", + BY_PROJECT = "by-project", +} + const ParamsSchema = { id: z.string(), }; @@ -98,7 +116,7 @@ export async function loader({ request, params }: LoaderArgs) { }); return json({ - allProjects: summaryResponse.projects as Array, + allProjects: summaryResponse.projects || undefined, timePlan: result.time_plan, note: result.note, activities: result.activities, @@ -195,228 +213,306 @@ export default function TimePlanView() { : [] ); - const sortedActivities = sortTimePlanActivitiesNaturally( - loaderData.activities, - targetInboxTasksByRefId, - targetBigPlansByRefId - ); const sortedSubTimePlans = sortTimePlansNaturally( loaderData.subPeriodTimePlans ); + const [selectedView, setSelectedView] = useState( + inferDefaultSelectedView(topLevelInfo.workspace, loaderData.timePlan) + ); + useEffect(() => { + setSelectedView( + inferDefaultSelectedView(topLevelInfo.workspace, loaderData.timePlan) + ); + }, [topLevelInfo, loaderData]); + + const sortedProjects = sortProjectsByTreeOrder(loaderData.allProjects || []); + const allProjectsByRefId = new Map( + loaderData.allProjects?.map((p) => [p.ref_id, p]) + ); + + let extraControls: Array | undefined = undefined; + if ( + isWorkspaceFeatureAvailable( + topLevelInfo.workspace, + WorkspaceFeature.PROJECTS + ) + ) { + extraControls = [ + + + + , + ]; + } + return ( - - - Save - , - ]} - > - - - - The Date - - - - - - - - Period - - - - - - - - - - - New Inbox Task - , - - <> - {isWorkspaceFeatureAvailable( - topLevelInfo.workspace, - WorkspaceFeature.BIG_PLANS - ) && ( - , + + <> + {isWorkspaceFeatureAvailable( + topLevelInfo.workspace, + WorkspaceFeature.BIG_PLANS + ) && ( + + )} + , + + , + + <> + {isWorkspaceFeatureAvailable( + topLevelInfo.workspace, + WorkspaceFeature.BIG_PLANS + ) && ( + + )} + , + , + ]} + > + {selectedView === View.MERGED && ( + - ))} - - - - {loaderData.completedNontargetInboxTasks.length > 0 && ( - - + )} + + {selectedView === View.BY_PROJECT && ( + <> + {sortedProjects.map((p) => { + const theActivities = loaderData.activities.filter((ac) => { + switch (ac.target) { + case TimePlanActivityTarget.INBOX_TASK: + return ( + targetInboxTasksByRefId.get(ac.target_ref_id) + ?.project_ref_id === p.ref_id + ); + case TimePlanActivityTarget.BIG_PLAN: + return ( + targetBigPlansByRefId.get(ac.target_ref_id) + ?.project_ref_id === p.ref_id + ); + } + throw new Error("Should not get here"); + }); + + if (theActivities.length === 0) { + return null; + } + + const fullProjectName = + computeProjectHierarchicalNameFromRoot( + p, + allProjectsByRefId + ); + + return ( + + + {fullProjectName} + + + + + ); + })} + + )} - )} - {loaderData.completedNontargetBigPlans && - loaderData.completedNontargetBigPlans.length > 0 && ( - - 0 && ( + + )} - {sortedSubTimePlans.length > 0 && ( - - - - )} + {loaderData.completedNontargetBigPlans && + loaderData.completedNontargetBigPlans.length > 0 && ( + + + + )} + + {sortedSubTimePlans.length > 0 && ( + + + + )} - {loaderData.higherTimePlan && ( - - - - )} + {loaderData.higherTimePlan && ( + + + + )} - {loaderData.previousTimePlan && ( - - - - )} + {loaderData.previousTimePlan && ( + + + + )} + @@ -434,3 +530,58 @@ export const ErrorBoundary = makeErrorBoundary( () => `There was an error loading time plan #${useParams().id}. Please try again!` ); + +interface ActivityListProps { + topLevelInfo: TopLevelInfo; + timePlan: TimePlan; + activities: Array; + inboxTasksByRefId: Map; + bigPlansByRefId: Map; + activityDoneness: Record; +} + +function ActivityList(props: ActivityListProps) { + const sortedActivities = sortTimePlanActivitiesNaturally( + props.activities, + props.inboxTasksByRefId, + props.bigPlansByRefId + ); + + return ( + + {sortedActivities.map((entry) => ( + + ))} + + ); +} + +function inferDefaultSelectedView(workspace: Workspace, timePlan: TimePlan) { + if (!isWorkspaceFeatureAvailable(workspace, WorkspaceFeature.PROJECTS)) { + return View.MERGED; + } + + switch (timePlan.period) { + case RecurringTaskPeriod.DAILY: + case RecurringTaskPeriod.WEEKLY: + return View.MERGED; + case RecurringTaskPeriod.MONTHLY: + case RecurringTaskPeriod.QUARTERLY: + case RecurringTaskPeriod.YEARLY: + return View.BY_PROJECT; + } +} diff --git a/src/webui/app/routes/workspace/time-plans/$id/add-from-current-time-plans/$otherTimePlanId.tsx b/src/webui/app/routes/workspace/time-plans/$id/add-from-current-time-plans/$otherTimePlanId.tsx index 01965be8..9323000d 100644 --- a/src/webui/app/routes/workspace/time-plans/$id/add-from-current-time-plans/$otherTimePlanId.tsx +++ b/src/webui/app/routes/workspace/time-plans/$id/add-from-current-time-plans/$otherTimePlanId.tsx @@ -182,7 +182,8 @@ export default function TimePlanAddFromCurrentTimePlans() { const filteredOtherActivities = filterActivitiesByTargetStatus( loaderData.otherActivities, otherTargetInboxTasksByRefId, - otherTargetBigPlansByRefId + otherTargetBigPlansByRefId, + loaderData.otherActivityDoneness ); const sortedOtherActivities = sortTimePlanActivitiesNaturally( filteredOtherActivities,