From 6181eeb0c8e3807b0ee5fc6d11a40e9c3775a2a9 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Tue, 10 May 2022 22:45:38 +0200 Subject: [PATCH] fix --- .github/workflows/expo-main.yml | 61 +++++ .github/workflows/publish-web.yml | 25 -- .gitignore | 1 + app.config.js | 51 +++++ app.json | 34 --- eas.json | 25 ++ package.json | 2 +- src/app.tsx | 2 +- src/features/async/hooks.ts | 4 +- src/features/data/utils.ts | 25 ++ src/features/day/utils.ts | 2 +- src/features/location/context.ts | 4 +- src/features/overrides/hooks.ts | 42 +++- src/features/planner/algorithm/build-graph.ts | 13 +- .../planner/algorithm/construct-day.ts | 19 +- src/features/planner/algorithm/get-next.ts | 45 ++-- src/features/planner/algorithm/utils.ts | 12 +- src/features/planner/context.ts | 3 + src/features/planner/hooks.ts | 214 ++++++++++++++++-- src/features/planner/types.ts | 16 +- src/features/tasks/hooks.tsx | 21 +- src/ui/components/base/index.ts | 1 + src/ui/components/base/list/index.tsx | 53 +++++ ...t-modal.native.tsx => react-modal.web.tsx} | 0 src/ui/components/base/popup/index.tsx | 7 +- src/ui/components/base/row/cell.tsx | 3 +- src/ui/components/base/row/row.tsx | 8 +- src/ui/components/form/date/index.tsx | 64 ++++++ src/ui/components/form/index.ts | 1 + src/ui/components/form/time/index.tsx | 23 +- src/ui/components/plan/day/index.tsx | 30 +++ src/ui/components/plan/day/task.tsx | 91 ++++++++ src/ui/components/plan/index.ts | 1 + src/ui/components/tasks/group/index.tsx | 2 +- src/ui/router/router.tsx | 14 ++ src/ui/router/types.ts | 25 +- src/ui/screens/locations/list.tsx | 28 +-- src/ui/screens/more/index.tsx | 8 +- src/ui/screens/plan/index.tsx | 57 +++++ src/ui/screens/task/add.tsx | 14 +- src/ui/screens/task/list.tsx | 32 +++ src/ui/screens/task/overrides.tsx | 171 ++++++++++++++ src/utils/data-context.tsx | 8 +- yarn.lock | 57 ++++- 44 files changed, 1099 insertions(+), 220 deletions(-) create mode 100644 .github/workflows/expo-main.yml delete mode 100644 .github/workflows/publish-web.yml create mode 100644 app.config.js delete mode 100644 app.json create mode 100644 eas.json create mode 100644 src/ui/components/base/list/index.tsx rename src/ui/components/base/modal/{react-modal.native.tsx => react-modal.web.tsx} (100%) create mode 100644 src/ui/components/form/date/index.tsx create mode 100644 src/ui/components/plan/day/index.tsx create mode 100644 src/ui/components/plan/day/task.tsx create mode 100644 src/ui/components/plan/index.ts create mode 100644 src/ui/screens/plan/index.tsx create mode 100644 src/ui/screens/task/list.tsx create mode 100644 src/ui/screens/task/overrides.tsx diff --git a/.github/workflows/expo-main.yml b/.github/workflows/expo-main.yml new file mode 100644 index 0000000..b4971dd --- /dev/null +++ b/.github/workflows/expo-main.yml @@ -0,0 +1,61 @@ +name: Expo Publish +on: + workflow_dispatch: + push: + branches: + - main + - v3 +jobs: + publish-web: + name: Publish web version + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. + with: + persist-credentials: false + + - name: Install and Build 🔧 + run: | + yarn install + yarn expo build:web + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@4.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: gh-pages + folder: web-build + publish-native: + name: Publish native versions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v2 + with: + node-version: 14.x + + - uses: expo/expo-github-action@v6 + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - run: | + git config --global user.email "github-action@example.com" + git config --global user.name "Github Bot" + yarn version --new-version=$BUILD_VERSION + env: + BUILD_VERSION: 1.${{ github.run_id }}.${{ github.run_number }} + - run: yarn install + + - run: echo $BUILD_VERSION + + # - run: eas build -p android --non-interactive + + - run: eas build -p ios --non-interactive + env: + EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }} + + - run: eas submit --platform ios --non-interactive --latest + env: + EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }} diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml deleted file mode 100644 index 9f30c79..0000000 --- a/.github/workflows/publish-web.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Build and Deploy web -on: - workflow_dispatch: - push: - branches: - - main -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout 🛎️ - uses: actions/checkout@v2.3.1 - with: - persist-credentials: false - - - name: Install and Build 🔧 - run: | - yarn install - NODE_ENV=production yarn expo build:web - - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@4.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - branch: gh-pages - folder: web-build diff --git a/.gitignore b/.gitignore index ec8a36a..c72bff9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/*.log node_modules/ .expo/ dist/ diff --git a/app.config.js b/app.config.js new file mode 100644 index 0000000..f8b71b9 --- /dev/null +++ b/app.config.js @@ -0,0 +1,51 @@ +const pkg = require('./package.json'); +const config = { + expo: { + name: 'Bob', + slug: 'bob', + version: pkg.version, + orientation: 'portrait', + icon: './assets/images/icon.png', + scheme: 'bobthealgorithm', + userInterfaceStyle: 'automatic', + splash: { + image: './assets/images/splash.png', + resizeMode: 'contain', + backgroundColor: '#ffffff', + }, + updates: { + fallbackToCacheTimeout: 0, + }, + assetBundlePatterns: ['**/*'], + ios: { + supportsTablet: true, + bundleIdentifier: 'pro.mortenolsen.bob', + buildNumber: pkg.version, + config: { + usesNonExemptEncryption: false, + }, + }, + android: { + adaptiveIcon: { + foregroundImage: './assets/images/adaptive-icon.png', + backgroundColor: '#ffffff', + }, + package: 'pro.mortenolsen.bob', + }, + web: { + favicon: './assets/images/favicon.png', + }, + // hooks: { + // postPublish: [ + // { + // file: 'sentry-expo/upload-sourcemaps', + // config: { + // setCommits: true, + // }, + // }, + // ], + // }, + }, +}; + +module.exports = config; diff --git a/app.json b/app.json deleted file mode 100644 index f805048..0000000 --- a/app.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "expo": { - "name": "bob", - "slug": "bob", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/images/icon.png", - "scheme": "myapp", - "userInterfaceStyle": "automatic", - "splash": { - "image": "./assets/images/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "updates": { - "fallbackToCacheTimeout": 0 - }, - "assetBundlePatterns": [ - "**/*" - ], - "ios": { - "supportsTablet": true - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive-icon.png", - "backgroundColor": "#ffffff" - } - }, - "web": { - "favicon": "./assets/images/favicon.png" - } - } -} diff --git a/eas.json b/eas.json new file mode 100644 index 0000000..8d66159 --- /dev/null +++ b/eas.json @@ -0,0 +1,25 @@ +{ + "cli": { + "version": ">= 0.42.4" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal" + }, + "production": { + } + }, + "submit": { + "production": { + "ios": { + "appleId": "morten@olsen.pro", + "appleTeamId": "D2944KU2BE", + "ascAppId": "1623552387" + } + } + } +} diff --git a/package.json b/package.json index a31e701..83ef5a2 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-dom": "17.0.1", "react-native": "0.64.3", "react-native-calendar-strip": "^2.2.5", + "react-native-calendars": "^1.1284.0", "react-native-collapsible": "^1.6.0", "react-native-gesture-handler": "^2.4.2", "react-native-get-random-values": "^1.8.0", @@ -61,7 +62,6 @@ "@types/chroma-js": "^2.1.3", "@types/react": "~17.0.21", "@types/react-dom": "^18.0.3", - "@types/react-native": "^0.67.6", "@types/styled-components-react-native": "^5.1.3", "babel-plugin-module-resolver": "^4.1.0", "expo-cli": "^5.4.3", diff --git a/src/app.tsx b/src/app.tsx index be7aa74..dc89c35 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -11,7 +11,7 @@ const App: React.FC = () => { async (from: any, to: any) => ({ to, from, - time: 45 * 60 * 1000, + time: 45, usableTime: 0, }), [], diff --git a/src/features/async/hooks.ts b/src/features/async/hooks.ts index 4d57394..b8ff774 100644 --- a/src/features/async/hooks.ts +++ b/src/features/async/hooks.ts @@ -47,7 +47,7 @@ const useAsyncCallback = < setLoading(false); } }, - [setLoading, setError, setResult, action], + [setLoading, setError, setResult, action, ...deps], ); const options = useMemo( @@ -63,7 +63,7 @@ const useAsyncCallback = < ]; return output; }, - [invoke, result, loading, error, prevArgs], + [invoke, result, loading, error, prevArgs, ...deps], ); return options; diff --git a/src/features/data/utils.ts b/src/features/data/utils.ts index dcd7b84..2156534 100644 --- a/src/features/data/utils.ts +++ b/src/features/data/utils.ts @@ -26,12 +26,37 @@ const stringToTime = (input: string) => { return result; }; +const largerThan = (a: Time, b: Time) => { + return timeToMinutes(a) > timeToMinutes(b); +} + +const max = (a: Time, b: Time) => largerThan(a, b) ? a : b; +const min = (a: Time, b: Time) => largerThan(a, b) ? b : a; + const timeToString = (input: Time) => `${input.hour}:${input.minute}`; +const timeToMinutes = (time: Time) => time.hour * 60 + time.minute; + +const minutesToTime = (minutes: number): Time => { + const hour = Math.floor(minutes / 60); + const minute = minutes % 60; + return { hour, minute }; +} + +const add = (a: Time, b: Time | number) => { + const toAdd = typeof b === 'number' ? b : b.hour * 60 + b.minute + const current = a.hour * 60 + a.minute + toAdd; + return minutesToTime(current); +} + const timeUtils = { timeToString, stringToTime, equal, + largerThan, + timeToMinutes, + max, + add, }; export { timeUtils }; diff --git a/src/features/day/utils.ts b/src/features/day/utils.ts index 35d016d..4d046ab 100644 --- a/src/features/day/utils.ts +++ b/src/features/day/utils.ts @@ -17,7 +17,7 @@ const dateToDay = (input: Date) => { } const toId = (day: Day) => { - return `${day.year}-${day.month}-${day.date}`; + return `${day.year.toString().padStart(4, '0')}-${day.month.toString().padStart(2, '0')}-${day.date.toString().padStart(2, '0')}`; } const dayUtils = { diff --git a/src/features/location/context.ts b/src/features/location/context.ts index 281d289..9cc566d 100644 --- a/src/features/location/context.ts +++ b/src/features/location/context.ts @@ -1,4 +1,4 @@ -import { UserLocation } from "../data"; +import { Time, UserLocation } from "../data"; import { createContext } from "react" type Transition = { @@ -11,7 +11,7 @@ type Transition = { type GetTransition = ( from: UserLocation, to: UserLocation, - time: Date, + time: Time, ) => Promise; type LocationContextValue = { diff --git a/src/features/overrides/hooks.ts b/src/features/overrides/hooks.ts index c0a97e9..12ad5eb 100644 --- a/src/features/overrides/hooks.ts +++ b/src/features/overrides/hooks.ts @@ -1,7 +1,7 @@ import { useContext } from "react" import { useAsyncCallback } from "../async"; import { Time } from "../data"; -import { OverrideContext } from "./context" +import { Override, OverrideContext } from "./context" export const useOverrides = () => { const { overrides } = useContext(OverrideContext); @@ -13,6 +13,46 @@ export const useSetOverride = () => { return set; } +export const useGetOverride = () => { + const { get } = useContext(OverrideContext); + return get; +} + +export const useSetTaskOverride = () => { + const { set } = useContext(OverrideContext); + const setTaskOverride = useAsyncCallback( + async (id: string, overrides: Override) => { + set(current => ({ + ...current, + tasks: { + ...current.tasks, + [id]: overrides, + }, + })); + }, + [set], + ); + return setTaskOverride; +} + +export const useClearTaskOverride = () => { + const { set } = useContext(OverrideContext); + const clearTaskOverride = useAsyncCallback( + async (id: string) => { + set(current => { + const tasks = {...current.tasks}; + delete tasks[id] + return { + ...current, + tasks, + }; + }); + }, + [set], + ); + return clearTaskOverride; +} + export const useStartTimeOverride = () => { const { overrides } = useContext(OverrideContext); return overrides.startTime; diff --git a/src/features/planner/algorithm/build-graph.ts b/src/features/planner/algorithm/build-graph.ts index 0138a7a..6c03bb8 100644 --- a/src/features/planner/algorithm/build-graph.ts +++ b/src/features/planner/algorithm/build-graph.ts @@ -1,6 +1,5 @@ -import { Context, GraphNode } from "#/types/graph"; -import { UserLocation } from "#/types/location"; -import { Task } from "#/types/task"; +import { Task, Time, UserLocation } from "#/features/data"; +import { Context, GraphNode } from "../types"; import { getImpossible, getNext } from "./get-next"; enum Strategies { @@ -29,7 +28,7 @@ type Status = RunningStatus | CompletedStatus; type BuildGraphOptions = { location: UserLocation; - time: Date; + time: Time; tasks: Task[]; context: Context; strategy?: Strategies; @@ -49,7 +48,7 @@ const fil = ( for (let b = 0; b < fn.length; b++) { if (fn[b](input[i])) { output[b].push(input[i]); - continue; + break; } } } @@ -150,7 +149,9 @@ const buildGraph = async ({ return complete([fullComplete]); } } - deadList.push(...dead); + if (strategy !== Strategies.all) { + deadList.push(...dead); + } } return complete(completedList); diff --git a/src/features/planner/algorithm/construct-day.ts b/src/features/planner/algorithm/construct-day.ts index 1015dff..abac90d 100644 --- a/src/features/planner/algorithm/construct-day.ts +++ b/src/features/planner/algorithm/construct-day.ts @@ -1,20 +1,16 @@ -import { GraphNode } from "#/types/graph"; -import { PlanItem } from "#/types/plans"; +import { timeUtils } from "#/features/data"; +import { GraphNode, PlannedEntry } from "../types"; const constructDay = (node: GraphNode) => { let current: GraphNode | undefined = node; - const plans: PlanItem[] = []; + const plans: PlannedEntry[] = []; while(current) { if (current.task) { plans.push({ type: 'task', - name: current.task?.name || 'start', - external: current.task?.external, - start: new Date( - current.time.start.getTime() - + (current.transition?.time || 0), - ), + name: current.task?.title || 'start', + start: timeUtils.add(current.time.start, (current.transition?.time || 0)), end: current.time.end, score: current.score, }) @@ -23,10 +19,7 @@ const constructDay = (node: GraphNode) => { plans.push({ type: 'transition', start: current.time.start, - end: new Date( - current.time.start.getTime() - + current.transition.time, - ), + end: timeUtils.add(current.time.start, current.transition.time), from: current.transition.from, to: current.transition.to, }) diff --git a/src/features/planner/algorithm/get-next.ts b/src/features/planner/algorithm/get-next.ts index 0a1667d..1106880 100644 --- a/src/features/planner/algorithm/get-next.ts +++ b/src/features/planner/algorithm/get-next.ts @@ -1,8 +1,10 @@ -import { GraphNode, Context } from '#/types/graph'; -import { Transition } from '#/types/location'; -import { Task } from '#/types/task'; +import { Task, Time, timeUtils } from '#/features/data'; +import { Transition } from '#/features/location'; +import { Context, GraphNode } from '../types'; import { getRemainingLocations, listContainLocation } from './utils'; +const DEFAULT_PRIORITY = 50; + const isDead = (impossible: Task[]) => { const missingRequered = impossible.find(t => t.required); return !!missingRequered; @@ -15,7 +17,7 @@ type GetImpossibleResult = { export const getImpossible = ( tasks: Task[], - time: Date, + time: Time, ) => { const result: GetImpossibleResult = { remaining: [], @@ -23,7 +25,7 @@ export const getImpossible = ( } for (let task of tasks) { - if (time > task.start.max) { + if (timeUtils.largerThan(time, task.startTime.max)) { result.impossible.push(task); } else { result.remaining.push(task); @@ -47,17 +49,17 @@ const calculateScore = ({ let score = 0; tasks?.forEach((task) => { - score += task.priority * 10; + score += (task.priority || DEFAULT_PRIORITY) * 10; impossible.forEach((task) => { if (task.required) { - score -= 10000 + (1 * task.priority); + score -= 10000 + (1 * (task.priority || DEFAULT_PRIORITY)); } else { - score -= 100 + (1 * task.priority); + score -= 100 + (1 * (task.priority || DEFAULT_PRIORITY)); } }); }); if (transition) { - const minutes = transition.time / 1000 / 60 + const minutes = transition.time; score -= 10 + (1 * minutes); } return score; @@ -71,7 +73,7 @@ const getNext = async ( const remainingLocations = getRemainingLocations(currentNode.remainingTasks, currentNode.location); await Promise.all(remainingLocations.map(async(location) => { const transition = await context.getTransition(currentNode.location, location, currentNode.time.end); - const endTime = new Date(currentNode.time.end.getTime() + transition.time); + const endTime = timeUtils.add(currentNode.time.end, transition.time); const { remaining, impossible } = getImpossible(currentNode.remainingTasks, endTime); const score = calculateScore({ transition, @@ -89,7 +91,7 @@ const getNext = async ( score: currentNode.score + score, status: { completed: false, - dead: isDead(impossible), + dead: false, // TODO: fix isDead(impossible), }, time: { start: currentNode.time.end, @@ -101,21 +103,14 @@ const getNext = async ( const possibleTasks = currentNode.remainingTasks.filter(task => !task.locations || listContainLocation(task.locations, currentNode.location)) await Promise.all(possibleTasks.map(async (orgTask) => { const task = {...orgTask}; - task.count = (task.count || 1) - 1 - let startTime = new Date( - Math.max( - currentNode.time.end.getTime(), - task.start.min.getTime(), - ), - ); + let startTime = + timeUtils.max( + currentNode.time.end, + task.startTime.min, + ); const parentRemainging = currentNode.remainingTasks.filter(t => t !== orgTask); - let endTime = new Date(startTime.getTime() + task.duration.min); - const { remaining, impossible } = getImpossible( - task.count > 0 - ? [...parentRemainging, task] - : parentRemainging, - endTime, - ); + let endTime = timeUtils.add(startTime, task.duration); + const { remaining, impossible } = getImpossible(parentRemainging, endTime); const score = calculateScore({ tasks: [task], impossible, diff --git a/src/features/planner/algorithm/utils.ts b/src/features/planner/algorithm/utils.ts index 73b2b55..20e9a94 100644 --- a/src/features/planner/algorithm/utils.ts +++ b/src/features/planner/algorithm/utils.ts @@ -5,12 +5,12 @@ export const locationEqual = (a: UserLocation, b: UserLocation) => { if (a === b) { return true; } - if (a.location === b.location) { - return true; - } - if (a.location && b.location && a.location.latitude === b.location.latitude && a.location.longitute === b.location.longitute) { - return true; - } + // if (a.location === b.location) { + // return true; + // } + // if (a.location && b.location && a.location.latitude === b.location.latitude && a.location.longitute === b.location.longitute) { + // return true; + // } if (a.title === b.title) { return true; } diff --git a/src/features/planner/context.ts b/src/features/planner/context.ts index 3f2b31e..6934142 100644 --- a/src/features/planner/context.ts +++ b/src/features/planner/context.ts @@ -1,8 +1,10 @@ import { createDataContext } from '#/utils/data-context'; +import { Time } from '../data'; import { Strategies } from "./algorithm/build-graph"; type PlannerOptions = { strategy: Strategies; + startTime: Time; } const { @@ -10,6 +12,7 @@ const { Provider: PlannerProvider, } = createDataContext({ createDefault: () => ({ + startTime: { hour: 7, minute: 0 }, strategy: Strategies.firstComplet, }), }); diff --git a/src/features/planner/hooks.ts b/src/features/planner/hooks.ts index 83c5511..44ab009 100644 --- a/src/features/planner/hooks.ts +++ b/src/features/planner/hooks.ts @@ -1,22 +1,129 @@ import { buildGraph, Status, Strategies } from "./algorithm/build-graph"; import { useContext } from "react"; -import { PlanItem } from "#/types/plans"; +import { add } from 'date-fns'; import { PlannerContext } from "./context"; -import { Task, UserLocation } from "../data"; +import { Task, Time, UserLocation } from "../data"; +import { useRoutines } from "../routines"; +import { useGoals } from "../goals/hooks"; +import { useAsyncCallback } from "../async"; +import { Day, dayUtils } from "../day"; +import { useGetOverride } from "../overrides"; +import { useGetAppointments } from "../appointments"; +import { useGetTransition } from "../location"; +import { PlannedEntry } from "./types"; +import { constructDay } from "./algorithm/construct-day"; -export type UsePlanOptions = { +export type PreparePlanOptions = { + start: Day; + end: Day; +} + +export type PlanOptions = PreparePlanOptions & { location: UserLocation; } -export type UsePlan = [ - (start?: Date) => Promise, - { - result?: { agenda: PlanItem[], impossible: Task[] }; - status?: Status; - loading: boolean; - error?: any; +export type PlanResultDay = { + day: Day; + start: Time; +} & ({ + status: 'waiting', +} | { + status: 'running', + nodes: number; + strategy: Strategies; +} | { + status: 'done'; + nodes: number; + strategy: Strategies; + plan: PlannedEntry[]; + impossible: Task[]; +}); + +export type PlanResult = { + impossible: Task[]; + days: { + [day: string]: PlanResultDay; } -] +} + +const getDays = (start: Day, end: Day): Day[] => { + const result: Day[] = []; + let currentDate = dayUtils.dayToDate(start); + const stopDate = dayUtils.dayToDate(end); + while (currentDate <= stopDate) { + result.push(dayUtils.dateToDay(currentDate)); + currentDate = add(currentDate, { + days: 1, + }); + } + return result; +} + +const firstValue = (...args: (T | undefined)[]): T => { + for (let arg of args) { + if (typeof arg !== 'undefined') { + return arg; + } + } + return undefined as unknown as T; +} + +export const useOptions = () => { + const { data } = useContext(PlannerContext); + return data; +} + +export const useSetOptions = () => { + const { setData } = useContext(PlannerContext); + return setData; +} + +const usePreparePlan = () => { + const routines = useRoutines(); + const goals = useGoals(); + const getOverrides = useGetOverride(); + const [getAppontments] = useGetAppointments(); + + const preparePlan = useAsyncCallback( + async ({ start, end }: PreparePlanOptions) => { + const days = await Promise.all(getDays(start, end).map(async (day) => { + const overrides = await getOverrides(day); + const start: Time = firstValue(overrides.startTime, { hour: 7, minute: 0 }); + const appointments = await getAppontments(day); + const tasks = [...routines, ...appointments].map((task) => { + const override = overrides.tasks[task.id]; + if (override?.enabled === false) { + return undefined; + } + const result: Task = { + ...task, + startTime: { + min: firstValue(override?.startMin, task.startTime.min), + max: firstValue(override?.startMax, task.startTime.max), + }, + duration: firstValue(override?.duration, task.duration), + required: firstValue(override?.required, task.required), + } + return result; + }).filter(Boolean).map(a => a as Exclude); + + return { + day, + start, + tasks, + } + + })); + return { + goals: [...goals], + days, + } + }, + [routines, goals, getOverrides, getAppontments], + ); + + return preparePlan; +} export const usePlanOptions = () => { const { data } = useContext(PlannerContext); @@ -29,6 +136,87 @@ export const useSetPlanOptions = () => { } export const usePlan = () => { - -} + const [preparePlan] = usePreparePlan(); + const getTransition = useGetTransition(); + const options = usePlanOptions(); + const createPlan = useAsyncCallback( + async ({ location, ...prepareOptions}: PlanOptions) => { + const prepared = await preparePlan(prepareOptions); + let result: PlanResult = { + impossible: [], + days: prepared.days.reduce((output, current) => ({ + ...output, + [dayUtils.toId(current.day)]: { + day: current.day, + start: current.start, + status: 'waiting', + }, + }), {} as {[name: string]: PlanResultDay}) + } + const update = (next: PlanResult) => { + result = next; + } + for (let day of prepared.days) { + const id = dayUtils.toId(day.day); + const dayGoal = prepared.goals; + const graph = await buildGraph({ + location, + time: day.start, + tasks: [...day.tasks, ...dayGoal], + strategy: options.strategy, + context: { + getTransition, + }, + callback: (status) => { + update({ + ...result, + days: { + ...result.days, + [id]: { + day: day.day, + start: day.start, + status: 'running', + nodes: status.nodes, + strategy: status.strategy, + } + } + }); + } + }); + const [winner] = graph; + if (!winner) { + continue; + } + const plan = constructDay(winner); + update({ + ...result, + days: { + ...result.days, + [id]: { + ...result.days[id], + impossible: winner.impossibeTasks, + status: 'done', + plan, + } + } + }) + prepared.goals = prepared.goals.filter((goal) => { + if (!dayGoal.find(d => d.id === goal.id)) { + return true; + } + if (!winner.impossibeTasks.find(d => d.id === goal.id)) { + return false; + } + return true; + }) + } + return { + ...result, + impossible: prepared.goals, + }; + }, + [preparePlan, getTransition, options], + ); + return createPlan; +} diff --git a/src/features/planner/types.ts b/src/features/planner/types.ts index 5e35872..0c5b4fb 100644 --- a/src/features/planner/types.ts +++ b/src/features/planner/types.ts @@ -1,3 +1,5 @@ +import { Task, Time, UserLocation } from "../data"; +import { GetTransition, Transition } from "../location"; type Context = { getTransition: GetTransition; @@ -6,20 +8,22 @@ type Context = { export type PlannedTask = { type: 'task'; name: string; - start: Date; + start: Time; external?: boolean; - end: Date; + end: Time; score: number; } export type PlannedTransition = { type: 'transition'; - start: Date; - end: Date; + start: Time; + end: Time; from: UserLocation; to: UserLocation; }; +export type PlannedEntry = PlannedTask | PlannedTransition; + type GraphNode = { location: UserLocation; task?: Task; @@ -29,8 +33,8 @@ type GraphNode = { impossibeTasks: Task[]; score: number; time: { - start: Date; - end: Date; + start: Time; + end: Time; }; status: { dead: boolean; diff --git a/src/features/tasks/hooks.tsx b/src/features/tasks/hooks.tsx index 5d9b30e..7c68129 100644 --- a/src/features/tasks/hooks.tsx +++ b/src/features/tasks/hooks.tsx @@ -2,8 +2,8 @@ import { useMemo } from "react"; import { useAppointments } from "../appointments"; import { useAsyncCallback } from "../async"; import { Task, TaskType } from "../data"; -import { useGoals, useSetGoals } from "../goals/hooks"; -import { useRoutines, useSetRoutine } from "../routines"; +import { useGoals, useRemoveGoal, useSetGoals } from "../goals/hooks"; +import { useRemoveRoutine, useRoutines, useSetRoutine } from "../routines"; export const useTasks = (type?: TaskType) => { const [appointments] = useAppointments(); @@ -44,3 +44,20 @@ export const useSetTask = () => { ); return result; }; + +export const useRemoveTask = () => { + const removeRoutine = useRemoveRoutine(); + const removeGoal = useRemoveGoal(); + + const result = useAsyncCallback( + async (task: Task) => { + if (task.type === TaskType.routine) { + removeRoutine(task.id); + } else if (task.type === TaskType.goal) { + removeGoal(task.id); + } + }, + [removeRoutine, removeGoal], + ); + return result; +}; diff --git a/src/ui/components/base/index.ts b/src/ui/components/base/index.ts index be016f0..e6abd41 100644 --- a/src/ui/components/base/index.ts +++ b/src/ui/components/base/index.ts @@ -5,3 +5,4 @@ export * from './popup'; export * from './row'; export * from './button'; export * from './group'; +export * from './list'; diff --git a/src/ui/components/base/list/index.tsx b/src/ui/components/base/list/index.tsx new file mode 100644 index 0000000..26555a8 --- /dev/null +++ b/src/ui/components/base/list/index.tsx @@ -0,0 +1,53 @@ +import { FlatList } from "react-native"; +import { Button } from "../button"; +import { Icon } from "../icon"; +import { Cell, Row, RowProps } from "../row"; + +type ListProps = { + add?: () => void; + remove?: (item: T) => any; + getKey: (item: T) => string; + items: T[]; + render: (item: T) => RowProps; +} + +function List({ + add, + remove, + getKey, + items, + render, +}: ListProps) { + return ( + <> + {!!add &&