From fefef41873bacf3af002f40e145b323350e259ec Mon Sep 17 00:00:00 2001 From: starheim98 Date: Wed, 6 Nov 2024 16:38:09 +0100 Subject: [PATCH 1/4] copy answers from other contexts --- .../no/bekk/database/AnswerRepository.kt | 66 ++++++++++++++++++- .../no/bekk/database/CommentRepository.kt | 4 +- .../no/bekk/database/DatabaseContext.kt | 1 + .../kotlin/no/bekk/routes/ContextRouting.kt | 4 ++ frontend/beCompliant/src/api/apiConfig.ts | 9 +++ .../createContextPage/CopyContextDropdown.tsx | 43 ++++++++++++ .../src/hooks/useFetchTeamTableContexts.ts | 15 +++++ .../beCompliant/src/hooks/useSubmitContext.ts | 1 + .../src/pages/CreateContextPage.tsx | 8 ++- .../src/pages/LockedCreateContextPage.tsx | 13 ++++ .../src/pages/UnlockedCreateContextPage.tsx | 10 +++ 11 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 frontend/beCompliant/src/components/createContextPage/CopyContextDropdown.tsx create mode 100644 frontend/beCompliant/src/hooks/useFetchTeamTableContexts.ts diff --git a/backend/src/main/kotlin/no/bekk/database/AnswerRepository.kt b/backend/src/main/kotlin/no/bekk/database/AnswerRepository.kt index 02a0d989d..a418ebbb4 100644 --- a/backend/src/main/kotlin/no/bekk/database/AnswerRepository.kt +++ b/backend/src/main/kotlin/no/bekk/database/AnswerRepository.kt @@ -4,6 +4,8 @@ package no.bekk.database import no.bekk.configuration.Database import no.bekk.util.logger import java.sql.SQLException +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.util.* object AnswerRepository { @@ -41,7 +43,7 @@ object AnswerRepository { ) ) } - logger.info("Successfully fetched context's $contextId answers from database.") + logger.debug("Successfully fetched context's $contextId answers from database.") } } catch (e: SQLException) { logger.error("Error fetching answers from database for contextId: $contextId. ${e.message}", e) @@ -83,7 +85,7 @@ object AnswerRepository { ) ) } - logger.info("Successfully fetched context's $contextId answers with record id $recordId from database.") + logger.debug("Successfully fetched context's $contextId answers with record id $recordId from database.") } } catch (e: SQLException) { logger.error( @@ -95,6 +97,66 @@ object AnswerRepository { return answers } + fun copyAnswersFromOtherContext(newContextId: String, contextToCopy: String) { + logger.info("Copying most recent answers from context $contextToCopy to new context $newContextId") + val mostRecentAnswers = getLatestAnswersByContextIdFromDatabase(contextToCopy) + + mostRecentAnswers.forEach { answer -> + try { + insertAnswerOnContext( + DatabaseAnswerRequest( + actor = answer.actor, + recordId = answer.recordId, + questionId = answer.questionId, + answer = answer.answer, + answerType = answer.answerType, + answerUnit = answer.answerUnit, + contextId = newContextId + ) + ) + logger.info("Answer for questionId ${answer.questionId} copied to context $newContextId") + } catch (e: SQLException) { + logger.error("Error copying answer for questionId ${answer.questionId} to context $newContextId: ${e.message}", e) + throw RuntimeException("Error copying answers to new context", e) + } + } + } + + private fun getLatestAnswersByContextIdFromDatabase(contextId: String): MutableList { + logger.debug("Fetching latest answers from database for contextId: $contextId") + val answers = mutableListOf() + try { + Database.getConnection().use { conn -> + val statement = conn.prepareStatement(""" + SELECT DISTINCT ON (question_id) id, actor, record_id, question_id, answer, updated, answer_type, answer_unit + FROM answers + WHERE context_id = ? + ORDER BY question_id, updated DESC + """) + statement.setObject(1, UUID.fromString(contextId)) + val resultSet = statement.executeQuery() + while (resultSet.next()) { + answers.add( + DatabaseAnswer( + actor = resultSet.getString("actor"), + recordId = resultSet.getString("record_id"), + questionId = resultSet.getString("question_id"), + answer = resultSet.getString("answer"), + updated = resultSet.getObject("updated", java.time.LocalDateTime::class.java)?.toString() ?: "", + answerType = resultSet.getString("answer_type"), + answerUnit = resultSet.getString("answer_unit"), + contextId = contextId + ) + ) + } + logger.info("Successfully fetched latest context's $contextId answers from database.") + } + } catch (e: SQLException) { + logger.error("Error fetching latest answers from database for contextId: $contextId. ${e.message}", e) + throw RuntimeException("Error fetching latest answers from database", e) + } + return answers + } fun insertAnswerOnContext(answer: DatabaseAnswerRequest): DatabaseAnswer { require(answer.contextId != null) { diff --git a/backend/src/main/kotlin/no/bekk/database/CommentRepository.kt b/backend/src/main/kotlin/no/bekk/database/CommentRepository.kt index 52d29b0a1..69b31bd1a 100644 --- a/backend/src/main/kotlin/no/bekk/database/CommentRepository.kt +++ b/backend/src/main/kotlin/no/bekk/database/CommentRepository.kt @@ -34,7 +34,7 @@ object CommentRepository { ) ) } - logger.info("Successfully fetched context's $contextId comments from database.") + logger.debug("Successfully fetched context's $contextId comments from database.") } } catch (e: SQLException) { logger.error("Error fetching comments for context $contextId: ${e.message}") @@ -72,7 +72,7 @@ object CommentRepository { ) ) } - logger.info("Successfully fetched context's $contextId comments with recordId $recordId from database.") + logger.debug("Successfully fetched context's $contextId comments with recordId $recordId from database.") } } catch (e: SQLException) { logger.error("Error fetching comments for context $contextId with recordId $recordId: ${e.message}") diff --git a/backend/src/main/kotlin/no/bekk/database/DatabaseContext.kt b/backend/src/main/kotlin/no/bekk/database/DatabaseContext.kt index 734d9e461..753b7aaf3 100644 --- a/backend/src/main/kotlin/no/bekk/database/DatabaseContext.kt +++ b/backend/src/main/kotlin/no/bekk/database/DatabaseContext.kt @@ -15,4 +15,5 @@ data class DatabaseContextRequest( val teamId: String, val tableId: String, val name: String, + val copyContext: String? = null, ) \ No newline at end of file diff --git a/backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt b/backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt index 35312134e..96fca6e34 100644 --- a/backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt +++ b/backend/src/main/kotlin/no/bekk/routes/ContextRouting.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import no.bekk.authentication.hasContextAccess import no.bekk.authentication.hasTeamAccess +import no.bekk.database.AnswerRepository import no.bekk.database.ContextRepository import no.bekk.database.DatabaseContextRequest import no.bekk.database.UniqueConstraintViolationException @@ -28,6 +29,9 @@ fun Route.contextRouting() { return@post } val insertedContext = ContextRepository.insertContext(contextRequest) + if (contextRequest.copyContext != null) { + AnswerRepository.copyAnswersFromOtherContext(insertedContext.id, contextRequest.copyContext) + } call.respond(HttpStatusCode.Created, Json.encodeToString(insertedContext)) return@post } catch (e: UniqueConstraintViolationException) { diff --git a/frontend/beCompliant/src/api/apiConfig.ts b/frontend/beCompliant/src/api/apiConfig.ts index 7d520bb55..4a83e3ebf 100644 --- a/frontend/beCompliant/src/api/apiConfig.ts +++ b/frontend/beCompliant/src/api/apiConfig.ts @@ -87,5 +87,14 @@ export const apiConfig = { queryKey: (teamId: string) => [PATH_CONTEXTS, teamId], url: (teamId: string) => `${API_URL_CONTEXTS}?teamId=${teamId}`, }, + forTeamAndTable: { + queryKey: (teamId: string, tableId: string) => [ + PATH_CONTEXTS, + teamId, + tableId, + ], + url: (teamId: string, tableId: string) => + `${API_URL_CONTEXTS}?teamId=${teamId}&tableId=${tableId}`, + }, }, } as const; diff --git a/frontend/beCompliant/src/components/createContextPage/CopyContextDropdown.tsx b/frontend/beCompliant/src/components/createContextPage/CopyContextDropdown.tsx new file mode 100644 index 000000000..915510dd1 --- /dev/null +++ b/frontend/beCompliant/src/components/createContextPage/CopyContextDropdown.tsx @@ -0,0 +1,43 @@ +import { Box, FormControl, FormLabel, Select, Spinner } from '@kvib/react'; +import { useFetchTeamTableContexts } from '../../hooks/useFetchTeamTableContexts'; + +export function CopyContextDropdown({ + tableId, + teamId, + setCopyContext, +}: { + tableId: string; + teamId: string; + setCopyContext: (context: string) => void; +}) { + const { data: contexts, isLoading: contextsIsLoading } = + useFetchTeamTableContexts(teamId, tableId); + + if (contextsIsLoading) { + return ; + } + + return ( + + + Kopier svar fra eksisterende skjema + + + + + + ); +} diff --git a/frontend/beCompliant/src/hooks/useFetchTeamTableContexts.ts b/frontend/beCompliant/src/hooks/useFetchTeamTableContexts.ts new file mode 100644 index 000000000..29a4aa25e --- /dev/null +++ b/frontend/beCompliant/src/hooks/useFetchTeamTableContexts.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiConfig } from '../api/apiConfig'; +import { axiosFetch } from '../api/Fetch'; +import { Context } from './useFetchTeamContexts'; + +export function useFetchTeamTableContexts(teamId: string, tableId: string) { + return useQuery({ + queryKey: apiConfig.contexts.forTeamAndTable.queryKey(teamId, tableId), + queryFn: () => + axiosFetch({ + url: `${apiConfig.contexts.forTeamAndTable.url(teamId, tableId)}`, + }).then((response) => response.data), + enabled: !!teamId && !!tableId, + }); +} diff --git a/frontend/beCompliant/src/hooks/useSubmitContext.ts b/frontend/beCompliant/src/hooks/useSubmitContext.ts index 50265fcfa..fb329216e 100644 --- a/frontend/beCompliant/src/hooks/useSubmitContext.ts +++ b/frontend/beCompliant/src/hooks/useSubmitContext.ts @@ -8,6 +8,7 @@ type SubmitContextRequest = { teamId: string; tableId: string; name: string; + copyContext?: string; }; export interface SubmitContextResponse { diff --git a/frontend/beCompliant/src/pages/CreateContextPage.tsx b/frontend/beCompliant/src/pages/CreateContextPage.tsx index 6f8269df4..78cb1d2ec 100644 --- a/frontend/beCompliant/src/pages/CreateContextPage.tsx +++ b/frontend/beCompliant/src/pages/CreateContextPage.tsx @@ -5,7 +5,7 @@ import { useFetchUserinfo } from '../hooks/useFetchUserinfo'; import { useFetchTables } from '../hooks/useFetchTables'; import { Center, Heading, Icon, Spinner } from '@kvib/react'; import { useSubmitContext } from '../hooks/useSubmitContext'; -import { FormEvent, useCallback } from 'react'; +import { FormEvent, useCallback, useState } from 'react'; export const CreateContextPage = () => { const [search, setSearch] = useSearchParams(); @@ -25,6 +25,8 @@ export const CreateContextPage = () => { [search, setSearch] ); + const [copyContext, setCopyContext] = useState(''); + const { mutate: submitContext, isPending: isLoading } = useSubmitContext(); const { @@ -61,7 +63,7 @@ export const CreateContextPage = () => { event.preventDefault(); if (teamId && name && tableId) { submitContext( - { teamId, tableId, name }, + { teamId, tableId, name, copyContext }, { onSuccess: (data) => { if (redirect) { @@ -103,6 +105,7 @@ export const CreateContextPage = () => { setTableId={setTableId} name={name} teamId={teamId} + setCopyContext={setCopyContext} /> ) : ( { setTableId={setTableId} name={name} teamId={teamId} + setCopyContext={setCopyContext} /> ); }; diff --git a/frontend/beCompliant/src/pages/LockedCreateContextPage.tsx b/frontend/beCompliant/src/pages/LockedCreateContextPage.tsx index 480604a59..b24d503e1 100644 --- a/frontend/beCompliant/src/pages/LockedCreateContextPage.tsx +++ b/frontend/beCompliant/src/pages/LockedCreateContextPage.tsx @@ -12,6 +12,8 @@ import { import { Form } from 'react-router-dom'; import { FormEvent } from 'react'; import { Table } from '../api/types'; +import { CopyContextDropdown } from '../components/createContextPage/CopyContextDropdown'; +import { useSearchParams } from 'react-router-dom'; type Props = { userinfo: UserInfo; @@ -22,6 +24,7 @@ type Props = { setTableId: (newTableId: string) => void; name: string | null; teamId: string | null; + setCopyContext: (context: string) => void; }; export const LockedCreateContextPage = ({ @@ -33,10 +36,13 @@ export const LockedCreateContextPage = ({ setTableId, name, teamId, + setCopyContext, }: Props) => { const teamDisplayName = userinfo.groups.find( (group) => group.id === teamId )?.displayName; + const [search, setSearch] = useSearchParams(); + const tableId = search.get('tableId'); return ( + {tableId && tableId.trim() && teamId && teamId.trim() && ( + + )}