Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
7 changes: 6 additions & 1 deletion packages/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ type LaraConfig {
finishedWeekDayCount: Int!
}

type Suggestion {
text: String!
time: String!
}

type Query {
config: LaraConfig!

Expand Down Expand Up @@ -260,7 +265,7 @@ type Query {
"""
Get all Suggestions
"""
suggestions: [String!]!
suggestions: [Suggestion!]!

"""
Get all Trainees
Expand Down
19 changes: 17 additions & 2 deletions packages/api/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Maybe<GqlReport>>;
/** Get all Suggestions */
suggestions: Array<Scalars['String']['output']>;
suggestions: Array<GqlSuggestion>;
/** Get all Trainees */
trainees: Array<GqlTrainee>;
/** Get all Trainers */
Expand Down Expand Up @@ -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<Scalars['Boolean']['output']>;
Expand Down Expand Up @@ -626,6 +632,7 @@ export type GqlResolversTypes = ResolversObject<{
Report: ResolverTypeWrapper<Report>;
ReportStatus: GqlReportStatus;
String: ResolverTypeWrapper<Scalars['String']['output']>;
Suggestion: ResolverTypeWrapper<GqlSuggestion>;
Trainee: ResolverTypeWrapper<Trainee>;
Trainer: ResolverTypeWrapper<Trainer>;
TrainerTraineePayload: ResolverTypeWrapper<Omit<GqlTrainerTraineePayload, 'trainee' | 'trainer'> & { trainee: GqlResolversTypes['Trainee'], trainer: GqlResolversTypes['Trainer'] }>;
Expand Down Expand Up @@ -662,6 +669,7 @@ export type GqlResolversParentTypes = ResolversObject<{
Query: {};
Report: Report;
String: Scalars['String']['output'];
Suggestion: GqlSuggestion;
Trainee: Trainee;
Trainer: Trainer;
TrainerTraineePayload: Omit<GqlTrainerTraineePayload, 'trainee' | 'trainer'> & { trainee: GqlResolversParentTypes['Trainee'], trainer: GqlResolversParentTypes['Trainer'] };
Expand Down Expand Up @@ -812,7 +820,7 @@ export type GqlQueryResolvers<ContextType = Context, ParentType extends GqlResol
reportForTrainee?: Resolver<Maybe<GqlResolversTypes['Report']>, ParentType, ContextType, RequireFields<GqlQueryReportForTraineeArgs, 'id' | 'week' | 'year'>>;
reportForYearAndWeek?: Resolver<Maybe<GqlResolversTypes['Report']>, ParentType, ContextType, RequireFields<GqlQueryReportForYearAndWeekArgs, 'week' | 'year'>>;
reports?: Resolver<Array<Maybe<GqlResolversTypes['Report']>>, ParentType, ContextType, Partial<GqlQueryReportsArgs>>;
suggestions?: Resolver<Array<GqlResolversTypes['String']>, ParentType, ContextType>;
suggestions?: Resolver<Array<GqlResolversTypes['Suggestion']>, ParentType, ContextType>;
trainees?: Resolver<Array<GqlResolversTypes['Trainee']>, ParentType, ContextType>;
trainers?: Resolver<Array<GqlResolversTypes['Trainer']>, ParentType, ContextType>;
}>;
Expand All @@ -833,6 +841,12 @@ export type GqlReportResolvers<ContextType = Context, ParentType extends GqlReso
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type GqlSuggestionResolvers<ContextType = Context, ParentType extends GqlResolversParentTypes['Suggestion'] = GqlResolversParentTypes['Suggestion']> = ResolversObject<{
text?: Resolver<GqlResolversTypes['String'], ParentType, ContextType>;
time?: Resolver<GqlResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type GqlTraineeResolvers<ContextType = Context, ParentType extends GqlResolversParentTypes['Trainee'] = GqlResolversParentTypes['Trainee']> = ResolversObject<{
alexaSkillLinked?: Resolver<Maybe<GqlResolversTypes['Boolean']>, ParentType, ContextType>;
avatar?: Resolver<GqlResolversTypes['String'], ParentType, ContextType>;
Expand Down Expand Up @@ -924,6 +938,7 @@ export type GqlResolvers<ContextType = Context> = ResolversObject<{
PrintPayload?: GqlPrintPayloadResolvers<ContextType>;
Query?: GqlQueryResolvers<ContextType>;
Report?: GqlReportResolvers<ContextType>;
Suggestion?: GqlSuggestionResolvers<ContextType>;
Trainee?: GqlTraineeResolvers<ContextType>;
Trainer?: GqlTrainerResolvers<ContextType>;
TrainerTraineePayload?: GqlTrainerTraineePayloadResolvers<ContextType>;
Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/resolvers/entry.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@ import { generateEntry, reportDayEntryByEntryId, validateEntryUpdate } from '../
import { isReportEditable, reportsWithinApprenticeship } from '../services/report.service'
import { createT } from '../i18n'

const tmp: Record<string, number> = {}

export const entryTraineeResolver: GqlResolvers<TraineeContext> = {
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)

Expand Down
48 changes: 41 additions & 7 deletions packages/backend/src/resolvers/trainee.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -57,17 +56,52 @@ export const traineeTraineeResolver: GqlResolvers<TraineeContext> = {
},
Query: {
suggestions: async (_parent, _args, { currentUser }) => {
const tmp: Record<string, number> = {}
const textCountsWithTime = {} as Record<string, { count: number; duration: number; text: string }>

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<string, { count: number; duration: number; text: string }>
)
}

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))
Expand Down
63 changes: 47 additions & 16 deletions packages/frontend/src/components/suggestions.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLTextAreaElement | null>
submitSuggestion: (text: string) => void
submitTextSuggestion: (text: string) => void
submitTimeSuggestion: (text: string) => void
}

// Maximum number suggestions
const SUGGESTION_COUNT = 5

const Suggestions: React.FC<SuggestionProps> = ({ submitSuggestion, inputRef }) => {
const Suggestions: React.FC<SuggestionProps> = ({ 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<string[]>([])
const [suggestionsWithTimes, setSuggestionsWithTimes] = useState<
Array<{
text: string
time: string
}>
>([])

const handleInput = useCallback(() => {
if (!inputRef.current) {
Expand All @@ -33,11 +41,21 @@ const Suggestions: React.FC<SuggestionProps> = ({ 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
Expand Down Expand Up @@ -118,32 +136,45 @@ const Suggestions: React.FC<SuggestionProps> = ({ 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 (
<StyledSuggestionWrapper>
<StyledSuggestionList active={visible}>
{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 (
<StyledSuggestionItem key={index} onMouseDown={() => setWaitingForBlur(suggestion)}>
{suggestion.substr(0, matchIndex)}
<b>{suggestion.substr(matchIndex, input.length)}</b>
{suggestion.substr(matchIndex + input.length)}
</StyledSuggestionItem>
<>
<StyledSuggestionItem
key={index}
onMouseDown={() => {
setWaitingForBlur(suggestion.text)
setWaitingForBlurTimeSuggestion(suggestion.time)
}}
>
<b>{suggestion.text}</b>
<b style={{ marginLeft: Spacings.s }}>{suggestion.time}</b>
</StyledSuggestionItem>
</>
)
})}
</StyledSuggestionList>
Expand Down
18 changes: 16 additions & 2 deletions packages/frontend/src/components/text-time-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const TextTimeInput: React.FC<TextTimeInputProps> = ({ entry, disabled, onDelete
}
}, [autoFocus])

const acceptSuggestion = (inputText: string) => {
const acceptTextSuggestion = (inputText: string) => {
if (!timeInput.current || !textInput.current) {
return
}
Expand All @@ -126,6 +126,16 @@ const TextTimeInput: React.FC<TextTimeInputProps> = ({ 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
Expand Down Expand Up @@ -178,7 +188,11 @@ const TextTimeInput: React.FC<TextTimeInputProps> = ({ entry, disabled, onDelete
/>
</StyledTextTimeInputWrapper>
</StyledEntryContainer>
<Suggestions submitSuggestion={acceptSuggestion} inputRef={textInput} />
<Suggestions
submitTextSuggestion={acceptTextSuggestion}
submitTimeSuggestion={acceptTimeSuggestion}
inputRef={textInput}
/>
</>
)
}
Expand Down
15 changes: 12 additions & 3 deletions packages/frontend/src/graphql/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Maybe<Report>>;
/** Get all Suggestions */
suggestions: Array<Scalars['String']['output']>;
suggestions: Array<Suggestion>;
/** Get all Trainees */
trainees: Array<Trainee>;
/** Get all Trainers */
Expand Down Expand Up @@ -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<Scalars['Boolean']['output']>;
Expand Down Expand Up @@ -845,7 +851,7 @@ export type SignatureSettingsDataQuery = { __typename?: 'Query', currentUser?: {
export type SuggestionsDataQueryVariables = Exact<{ [key: string]: never; }>;


export type SuggestionsDataQuery = { __typename?: 'Query', suggestions: Array<string> };
export type SuggestionsDataQuery = { __typename?: 'Query', suggestions: Array<{ __typename?: 'Suggestion', text: string, time: string }> };

export type TraineePageDataQueryVariables = Exact<{ [key: string]: never; }>;

Expand Down Expand Up @@ -2065,7 +2071,10 @@ export type SignatureSettingsDataLazyQueryHookResult = ReturnType<typeof useSign
export type SignatureSettingsDataSuspenseQueryHookResult = ReturnType<typeof useSignatureSettingsDataSuspenseQuery>;
export const SuggestionsDataDocument = gql`
query SuggestionsData {
suggestions
suggestions {
text
time
}
}
`;
export function useSuggestionsDataQuery(baseOptions?: Apollo.QueryHookOptions<SuggestionsDataQuery, SuggestionsDataQueryVariables>) {
Expand Down
5 changes: 4 additions & 1 deletion packages/frontend/src/graphql/queries/suggestions-data.gql
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
query SuggestionsData {
suggestions
suggestions {
text
time
}
}