From c6edfa13dc183e6340c99b64e28b965618179c27 Mon Sep 17 00:00:00 2001 From: thegreenmilecomposer Date: Thu, 14 Aug 2025 16:57:06 +0200 Subject: [PATCH] feat(all): time duration is suggested along with textinput --- package.json | 2 +- packages/api/schema.gql | 7 ++- packages/api/src/graphql.ts | 19 +++++- .../backend/src/resolvers/entry.resolver.ts | 10 +++ .../backend/src/resolvers/trainee.resolver.ts | 48 +++++++++++--- .../frontend/src/components/suggestions.tsx | 63 ++++++++++++++----- .../src/components/text-time-input.tsx | 18 +++++- packages/frontend/src/graphql/index.tsx | 15 ++++- .../src/graphql/queries/suggestions-data.gql | 5 +- 9 files changed, 154 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index c01e0ff..5b0a7e2 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "sls": "sls offline start", "sort-package-jsons": "sort-package-json package.json packages/*/package.json", "sort-package-jsons:verify": "yarn sort-package-jsons && git diff --exit-code package.json packages/*/package.json", - "start": "concurrently -kn sls,frontend,admin -c blue,red,green yarn:sls yarn:lerna:start yarn:ddb:admin", + "start": "concurrently -n sls,frontend,admin -c blue,red,green yarn:sls yarn:lerna:start yarn:ddb:admin", "test": "lerna run --parallel test --stream", "verify": "yarn generate:verify && yarn format:verify && yarn lint:verify && yarn sort-package-jsons:verify" }, diff --git a/packages/api/schema.gql b/packages/api/schema.gql index 26c2a79..d7ed269 100644 --- a/packages/api/schema.gql +++ b/packages/api/schema.gql @@ -232,6 +232,11 @@ type LaraConfig { finishedWeekDayCount: Int! } +type Suggestion { + text: String! + time: String! +} + type Query { config: LaraConfig! @@ -260,7 +265,7 @@ type Query { """ Get all Suggestions """ - suggestions: [String!]! + suggestions: [Suggestion!]! """ Get all Trainees diff --git a/packages/api/src/graphql.ts b/packages/api/src/graphql.ts index c5db1c9..6af9755 100644 --- a/packages/api/src/graphql.ts +++ b/packages/api/src/graphql.ts @@ -358,7 +358,7 @@ export type GqlQuery = { /** Get all Reports for the current User. The result can be filtered by the 'statuses' attribut */ reports: Array>; /** Get all Suggestions */ - suggestions: Array; + suggestions: Array; /** Get all Trainees */ trainees: Array; /** Get all Trainers */ @@ -420,6 +420,12 @@ export type GqlReportStatus = /** Report is open */ | 'todo'; +export type GqlSuggestion = { + __typename?: 'Suggestion'; + text: Scalars['String']['output']; + time: Scalars['String']['output']; +}; + export type GqlTrainee = GqlUserInterface & { __typename?: 'Trainee'; alexaSkillLinked?: Maybe; @@ -626,6 +632,7 @@ export type GqlResolversTypes = ResolversObject<{ Report: ResolverTypeWrapper; ReportStatus: GqlReportStatus; String: ResolverTypeWrapper; + Suggestion: ResolverTypeWrapper; Trainee: ResolverTypeWrapper; Trainer: ResolverTypeWrapper; TrainerTraineePayload: ResolverTypeWrapper & { trainee: GqlResolversTypes['Trainee'], trainer: GqlResolversTypes['Trainer'] }>; @@ -662,6 +669,7 @@ export type GqlResolversParentTypes = ResolversObject<{ Query: {}; Report: Report; String: Scalars['String']['output']; + Suggestion: GqlSuggestion; Trainee: Trainee; Trainer: Trainer; TrainerTraineePayload: Omit & { trainee: GqlResolversParentTypes['Trainee'], trainer: GqlResolversParentTypes['Trainer'] }; @@ -812,7 +820,7 @@ export type GqlQueryResolvers, ParentType, ContextType, RequireFields>; reportForYearAndWeek?: Resolver, ParentType, ContextType, RequireFields>; reports?: Resolver>, ParentType, ContextType, Partial>; - suggestions?: Resolver, ParentType, ContextType>; + suggestions?: Resolver, ParentType, ContextType>; trainees?: Resolver, ParentType, ContextType>; trainers?: Resolver, ParentType, ContextType>; }>; @@ -833,6 +841,12 @@ export type GqlReportResolvers; }>; +export type GqlSuggestionResolvers = ResolversObject<{ + text?: Resolver; + time?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}>; + export type GqlTraineeResolvers = ResolversObject<{ alexaSkillLinked?: Resolver, ParentType, ContextType>; avatar?: Resolver; @@ -924,6 +938,7 @@ export type GqlResolvers = ResolversObject<{ PrintPayload?: GqlPrintPayloadResolvers; Query?: GqlQueryResolvers; Report?: GqlReportResolvers; + Suggestion?: GqlSuggestionResolvers; Trainee?: GqlTraineeResolvers; Trainer?: GqlTrainerResolvers; TrainerTraineePayload?: GqlTrainerTraineePayloadResolvers; diff --git a/packages/backend/src/resolvers/entry.resolver.ts b/packages/backend/src/resolvers/entry.resolver.ts index a5a247c..1e27f9b 100644 --- a/packages/backend/src/resolvers/entry.resolver.ts +++ b/packages/backend/src/resolvers/entry.resolver.ts @@ -8,10 +8,20 @@ import { generateEntry, reportDayEntryByEntryId, validateEntryUpdate } from '../ import { isReportEditable, reportsWithinApprenticeship } from '../services/report.service' import { createT } from '../i18n' +const tmp: Record = {} + export const entryTraineeResolver: GqlResolvers = { Mutation: { createEntry: async (_parent, { dayId, input }, { currentUser }) => { const reports = await reportsWithinApprenticeship(currentUser) + reports.forEach((report) => { + report.days.forEach((day) => { + day.entries.forEach((entry) => { + const text = entry.text + tmp[text] = tmp[text] ? tmp[text] + 1 : 1 + }) + }) + }) const { report, day } = reportDayByDayId(dayId, reports) diff --git a/packages/backend/src/resolvers/trainee.resolver.ts b/packages/backend/src/resolvers/trainee.resolver.ts index ccd86b3..b0a3b96 100644 --- a/packages/backend/src/resolvers/trainee.resolver.ts +++ b/packages/backend/src/resolvers/trainee.resolver.ts @@ -15,7 +15,6 @@ import { reportById } from '../repositories/report.repo' import { saveUser, userById } from '../repositories/user.repo' import { alexaSkillLinked } from '../services/alexa.service' import { company } from '../services/company.service' -import { entries } from '../services/entry.service' import { createPrintReportData, createPrintUserData, invokePrintLambda, savePrintData } from '../services/print.service' import { reportsWithinApprenticeship } from '../services/report.service' import { endOfToolUsage, startOfToolUsage, validateTrainee } from '../services/trainee.service' @@ -57,17 +56,52 @@ export const traineeTraineeResolver: GqlResolvers = { }, Query: { suggestions: async (_parent, _args, { currentUser }) => { - const tmp: Record = {} + const textCountsWithTime = {} as Record + + function convertToHours(minutes: number) { + const hours = Math.floor(minutes / 60) + return hours + } const reports = await reportsWithinApprenticeship(currentUser) - const traineeEntries = entries(reports) - traineeEntries.forEach(({ text }) => { - tmp[text] = tmp[text] ? tmp[text] + 1 : 1 + reports.forEach((report) => { + report.days.forEach((day) => { + day.entries.forEach((entry) => { + const { text, time } = entry + + if (!textCountsWithTime[text]) { + textCountsWithTime[text] = { count: 0, duration: time, text: text } + } + textCountsWithTime[text].count += 1 + textCountsWithTime[text].duration = time + }) + }) + }) + + const getFrequentTexts = () => { + return Object.entries(textCountsWithTime) + .filter(([_, data]) => data.count > 5) + .reduce( + (accumulator, [text, data]) => { + accumulator[text] = data + return accumulator + }, + {} as Record + ) + } + + const frequentTexts = getFrequentTexts() + + const sugg = Object.entries(frequentTexts).map(([text, data]) => { + const duration = convertToHours(data.duration) + return { + text, + time: duration.toString(), + } }) - const sortedEntries = Object.entries(tmp).sort((a, b) => a[1] - b[1]) - return sortedEntries.filter((entry) => entry[1] >= 5).map(([text]) => text) + return sugg }, print: async (_parent, { ids }, { currentUser }) => { const reports = await Promise.all(ids.map(reportById)) diff --git a/packages/frontend/src/components/suggestions.tsx b/packages/frontend/src/components/suggestions.tsx index 34f1d50..46ecfff 100644 --- a/packages/frontend/src/components/suggestions.tsx +++ b/packages/frontend/src/components/suggestions.tsx @@ -1,29 +1,37 @@ import React, { useCallback, useState } from 'react' -import { StyledSuggestionItem, StyledSuggestionList, StyledSuggestionWrapper } from '@lara/components' +import { Spacings, StyledSuggestionItem, StyledSuggestionList, StyledSuggestionWrapper } from '@lara/components' import { useSuggestionsDataQuery } from '../graphql' import { useFocusState } from '../hooks/use-focus-state' interface SuggestionProps { inputRef: React.RefObject - submitSuggestion: (text: string) => void + submitTextSuggestion: (text: string) => void + submitTimeSuggestion: (text: string) => void } // Maximum number suggestions const SUGGESTION_COUNT = 5 -const Suggestions: React.FC = ({ submitSuggestion, inputRef }) => { +const Suggestions: React.FC = ({ submitTextSuggestion, submitTimeSuggestion, inputRef }) => { const { loading, data } = useSuggestionsDataQuery() const inputFocused = useFocusState(inputRef) const [visible, setVisible] = React.useState(true) const [waitingForBlur, setWaitingForBlur] = React.useState('') + const [waitingForBlurTimeSuggestion, setWaitingForBlurTimeSuggestion] = React.useState('') // Ref states so the handleKeyDown handler can use the state const [focusIndex, setFocusIndex] = useState(-1) const [suggestions, setSuggestions] = useState([]) + const [suggestionsWithTimes, setSuggestionsWithTimes] = useState< + Array<{ + text: string + time: string + }> + >([]) const handleInput = useCallback(() => { if (!inputRef.current) { @@ -33,11 +41,21 @@ const Suggestions: React.FC = ({ submitSuggestion, inputRef }) // update suggestions on textInput change const input = inputRef.current.value.toLowerCase() - const newSuggestionState = + const filteredSuggestions = data?.suggestions - .filter((suggestion) => input.replace(/\s/g, '') !== '' && suggestion.toLowerCase().indexOf(input) > -1) + .filter((suggestion) => input.replace(/\s/g, '') !== '' && suggestion.text.toLowerCase().indexOf(input) > -1) .slice(0, SUGGESTION_COUNT) ?? [] + const newSuggestionState = filteredSuggestions.map((s) => s.text) + + // Single array containing both text and time + const suggestionsWithTimes = filteredSuggestions.map((s) => ({ + text: s.text, + time: s.time, + })) + + setSuggestionsWithTimes(suggestionsWithTimes) + setSuggestions(newSuggestionState) // Reduce focusIndex if less suggestions are valid @@ -118,32 +136,45 @@ const Suggestions: React.FC = ({ submitSuggestion, inputRef }) // This is nessecary so the blur event doesn't interfere // with the focus event on the timeInput field if (waitingForBlur) { - submitSuggestion(waitingForBlur) + submitTextSuggestion(waitingForBlur) setWaitingForBlur('') // clear the state setFocusIndex(-1) setSuggestions([]) } - }, [inputFocused, submitSuggestion, waitingForBlur]) + + if (waitingForBlurTimeSuggestion) { + submitTimeSuggestion(waitingForBlurTimeSuggestion) + setWaitingForBlurTimeSuggestion('') + + // clear the state + setFocusIndex(-1) + setSuggestions([]) + } + }, [inputFocused, submitTextSuggestion, submitTimeSuggestion, waitingForBlur, waitingForBlurTimeSuggestion]) return ( - {suggestions.map((suggestion, index) => { + {suggestionsWithTimes.map((suggestion, index) => { if (!inputRef.current) { return } - const input = inputRef.current.value.toLowerCase() - const matchIndex = suggestion.toLowerCase().indexOf(input) - return ( - setWaitingForBlur(suggestion)}> - {suggestion.substr(0, matchIndex)} - {suggestion.substr(matchIndex, input.length)} - {suggestion.substr(matchIndex + input.length)} - + <> + { + setWaitingForBlur(suggestion.text) + setWaitingForBlurTimeSuggestion(suggestion.time) + }} + > + {suggestion.text} + {suggestion.time} + + ) })} diff --git a/packages/frontend/src/components/text-time-input.tsx b/packages/frontend/src/components/text-time-input.tsx index 8311d11..42ec672 100644 --- a/packages/frontend/src/components/text-time-input.tsx +++ b/packages/frontend/src/components/text-time-input.tsx @@ -116,7 +116,7 @@ const TextTimeInput: React.FC = ({ entry, disabled, onDelete } }, [autoFocus]) - const acceptSuggestion = (inputText: string) => { + const acceptTextSuggestion = (inputText: string) => { if (!timeInput.current || !textInput.current) { return } @@ -126,6 +126,16 @@ const TextTimeInput: React.FC = ({ entry, disabled, onDelete resizeInput() } + const acceptTimeSuggestion = (inputTime: string) => { + if (!timeInput.current || !textInput.current) { + return + } + + timeInput.current.value = inputTime + setTimeInputValue(timeInput.current.value) + textInput.current.focus() + } + const resizeInput = () => { if (!textInput.current) { return @@ -178,7 +188,11 @@ const TextTimeInput: React.FC = ({ entry, disabled, onDelete /> - + ) } diff --git a/packages/frontend/src/graphql/index.tsx b/packages/frontend/src/graphql/index.tsx index 9c2f792..ee59736 100644 --- a/packages/frontend/src/graphql/index.tsx +++ b/packages/frontend/src/graphql/index.tsx @@ -357,7 +357,7 @@ export type Query = { /** Get all Reports for the current User. The result can be filtered by the 'statuses' attribut */ reports: Array>; /** Get all Suggestions */ - suggestions: Array; + suggestions: Array; /** Get all Trainees */ trainees: Array; /** Get all Trainers */ @@ -420,6 +420,12 @@ export enum ReportStatus { Todo = 'todo' } +export type Suggestion = { + __typename?: 'Suggestion'; + text: Scalars['String']['output']; + time: Scalars['String']['output']; +}; + export type Trainee = UserInterface & { __typename?: 'Trainee'; alexaSkillLinked?: Maybe; @@ -845,7 +851,7 @@ export type SignatureSettingsDataQuery = { __typename?: 'Query', currentUser?: { export type SuggestionsDataQueryVariables = Exact<{ [key: string]: never; }>; -export type SuggestionsDataQuery = { __typename?: 'Query', suggestions: Array }; +export type SuggestionsDataQuery = { __typename?: 'Query', suggestions: Array<{ __typename?: 'Suggestion', text: string, time: string }> }; export type TraineePageDataQueryVariables = Exact<{ [key: string]: never; }>; @@ -2065,7 +2071,10 @@ export type SignatureSettingsDataLazyQueryHookResult = ReturnType; export const SuggestionsDataDocument = gql` query SuggestionsData { - suggestions + suggestions { + text + time + } } `; export function useSuggestionsDataQuery(baseOptions?: Apollo.QueryHookOptions) { diff --git a/packages/frontend/src/graphql/queries/suggestions-data.gql b/packages/frontend/src/graphql/queries/suggestions-data.gql index 49f1cf6..307ba52 100644 --- a/packages/frontend/src/graphql/queries/suggestions-data.gql +++ b/packages/frontend/src/graphql/queries/suggestions-data.gql @@ -1,3 +1,6 @@ query SuggestionsData { - suggestions + suggestions { + text + time + } }