diff --git a/backend/Dockerfile b/backend/Dockerfile index b476154a5..d44a0a734 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -11,7 +11,6 @@ WORKDIR /app # Copy the built jar file from the build stage COPY --from=build /home/gradle/src/build/libs/*.jar /app/backend.jar -COPY config.properties /app/ COPY application.properties /app/ # Specify the command to run the application diff --git a/backend/README.md b/backend/README.md index 896b1b5df..a9d7ef1a3 100644 --- a/backend/README.md +++ b/backend/README.md @@ -39,6 +39,8 @@ Set up as follows: Working directory: /backend Use classpath of module: spire-kk.backend.main +Set the environment variable `AIRTABLE_ACCESS_TOKEN` in `Run -> Edit Configurations...`. You can get the value from one of your teammates. + ## Run the application To set up an IntelliJ project, New Project from existing sources -> val statement = conn.prepareStatement( - "SELECT id, actor, question, question_id, answer, updated, team FROM questions" + "SELECT id, actor, question, question_id, answer, updated, team, comment FROM questions" ) val resultSet = statement.executeQuery() while (resultSet.next()) { @@ -49,7 +50,7 @@ class DatabaseRepository { questionId = questionId, Svar = answer, updated = updated?.toString() ?: "", - team = team + team = team, ) ) } @@ -112,9 +113,9 @@ class DatabaseRepository { val resultSet = result.executeQuery() if (resultSet.next()) { - updateRow(conn, answer) + updateAnswerRow(conn, answer) } else { - insertRow(conn, answer) + insertAnswerRow(conn, answer) } } @@ -124,7 +125,7 @@ class DatabaseRepository { } } - private fun insertRow(conn: Connection, answer: Answer): Int { + private fun insertAnswerRow(conn: Connection, answer: Answer): Int { val sqlStatement = "INSERT INTO questions (actor, question, question_id, answer, team) VALUES (?, ?, ?, ?, ?)" @@ -138,7 +139,7 @@ class DatabaseRepository { } } - private fun updateRow(conn: Connection, answer: Answer): Int { + private fun updateAnswerRow(conn: Connection, answer: Answer): Int { val sqlStatement = "UPDATE questions SET actor = ?, question = ?, question_id = ?, answer = ?, team = ?, updated = CURRENT_TIMESTAMP WHERE question_id = ? AND team = ?" @@ -151,9 +152,98 @@ class DatabaseRepository { statement.setString(6, answer.questionId) statement.setString(7, answer.team) + return statement.executeUpdate() } } + fun getCommentsByTeamIdFromDatabase(teamId: String): MutableList { + val connection = getDatabaseConnection() + val comments = mutableListOf() + try { + connection.use { conn -> + val statement = conn.prepareStatement( + "SELECT id, actor, question_id, comment, updated, team FROM comments WHERE team = ?" + ) + statement.setString(1, teamId) + val resultSet = statement.executeQuery() + while (resultSet.next()) { + val actor = resultSet.getString("actor") + val questionId = resultSet.getString("question_id") + val comment = resultSet.getString("comment") + val updated = resultSet.getObject("updated", java.time.LocalDateTime::class.java) + val team = resultSet.getString("team") + comments.add( + Comment( + actor = actor, + questionId = questionId, + comment = comment, + updated = updated?.toString() ?: "", + team = team, + ) + ) + } + } + } catch (e: SQLException) { + e.printStackTrace() + throw RuntimeException("Error fetching comments from database", e) + } + return comments + } + fun getCommentFromDatabase(comment: Comment) { + val connection = getDatabaseConnection() + try { + connection.use { conn -> + + val result = conn.prepareStatement( + "SELECT question_id, team FROM comments WHERE question_id = ? AND team = ? " + ) + + result.setString(1, comment.questionId) + result.setString(2, comment.team) + val resultSet = result.executeQuery() + + if (resultSet.next()) { + updateCommentRow(conn, comment) + } else { + insertCommentRow(conn, comment) + } + + } + + } catch (e: SQLException) { + e.printStackTrace() + } + } + + private fun insertCommentRow(conn: Connection, comment: Comment): Int { + val sqlStatement = + "INSERT INTO comments (actor, question_id, comment, team) VALUES (?, ?, ?, ?)" + + conn.prepareStatement(sqlStatement).use { statement -> + statement.setString(1, comment.actor) + statement.setString(2, comment.questionId) + statement.setString(3, comment.comment) + statement.setString(4, comment.team) + return statement.executeUpdate() + } + } + + private fun updateCommentRow(conn: Connection, comment: Comment): Int { + val sqlStatement = + "UPDATE comments SET actor = ?, question_id = ?, team = ?, comment = ?, updated = CURRENT_TIMESTAMP WHERE question_id = ? AND team = ?" + + conn.prepareStatement(sqlStatement).use { statement -> + statement.setString(1, comment.actor) + statement.setString(2, comment.questionId) + statement.setString(3, comment.team) + statement.setString(4, comment.comment) + statement.setString(5, comment.questionId) + statement.setString(6, comment.team) + + + return statement.executeUpdate() + } + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/no/bekk/plugins/Routing.kt b/backend/src/main/kotlin/no/bekk/plugins/Routing.kt index c8c7f0c7f..81c1f0205 100644 --- a/backend/src/main/kotlin/no/bekk/plugins/Routing.kt +++ b/backend/src/main/kotlin/no/bekk/plugins/Routing.kt @@ -30,6 +30,13 @@ fun Application.configureRouting() { call.respondText("Velkommen til Kartverket Kontrollere!") } } + + routing { + get("/health") { + call.respondText("Health OK", ContentType.Text.Plain) + } + } + routing { get("/metodeverk") { val data = airTableController.fetchDataFromMetodeverk() @@ -47,13 +54,11 @@ fun Application.configureRouting() { val data = airTableController.fetchDataFromAlle() call.respondText(data.records.toString()) } - } routing { get("/{teamid}/kontrollere") { val teamid = call.parameters["teamid"] - if (teamid != null) { val data = airTableController.fetchDataFromMetodeverk() val meta = airTableController.fetchDataFromMetadata() @@ -68,7 +73,6 @@ fun Application.configureRouting() { } } - routing { get("/answers") { var answers = mutableListOf() @@ -97,8 +101,6 @@ fun Application.configureRouting() { } } - - routing { post("/answer") { val answerRequestJson = call.receiveText() @@ -116,8 +118,41 @@ fun Application.configureRouting() { } } + routing { + post("/comments") { + val commentRequestJson = call.receiveText() + val commentRequest = Json.decodeFromString(commentRequestJson) + val comment = Comment( + questionId = commentRequest.questionId, + comment = commentRequest.comment, + team = commentRequest.team, + updated = "", + actor = commentRequest.actor, + ) + databaseRepository.getCommentFromDatabase(comment) + call.respondText("Comment was successfully submitted.") + } + } + + routing { + get("/comments/{teamId}") { + val teamId = call.parameters["teamId"] + var comments: MutableList + if (teamId != null) { + comments = databaseRepository.getCommentsByTeamIdFromDatabase(teamId) + val commentsJson = Json.encodeToString(comments) + call.respondText(commentsJson, contentType = ContentType.Application.Json) + } else { + call.respond(HttpStatusCode.BadRequest, "Team id not found") + } + } + } + } +@Serializable +data class Answer(val actor: String, val questionId: String, val question: String, val Svar: String? = null, + val updated: String, val team: String?) @Serializable -data class Answer(val actor: String, val questionId: String, val question: String, val Svar: String, val updated: String, val team: String?) \ No newline at end of file +data class Comment(val actor: String, val questionId: String, val comment: String, val team: String?, val updated: String) \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.10__add_comment_table.sql b/backend/src/main/resources/db/migration/V1.10__add_comment_table.sql new file mode 100644 index 000000000..4d312057e --- /dev/null +++ b/backend/src/main/resources/db/migration/V1.10__add_comment_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS comments +( + id SERIAL PRIMARY KEY, + actor VARCHAR(255), + question_id VARCHAR(255), + comment VARCHAR(255), + team VARCHAR(255), + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.8__add_comment_column.sql b/backend/src/main/resources/db/migration/V1.8__add_comment_column.sql new file mode 100644 index 000000000..764b36479 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1.8__add_comment_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE questions +ADD COLUMN comment VARCHAR(255) \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.9__remove_comment_column.sql b/backend/src/main/resources/db/migration/V1.9__remove_comment_column.sql new file mode 100644 index 000000000..26dead20e --- /dev/null +++ b/backend/src/main/resources/db/migration/V1.9__remove_comment_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE questions +DROP COLUMN comment; \ No newline at end of file diff --git a/frontend/beCompliant/index.html b/frontend/beCompliant/index.html index 4684718c4..d5ab33bd4 100644 --- a/frontend/beCompliant/index.html +++ b/frontend/beCompliant/index.html @@ -1,10 +1,10 @@ - + - BeCompliant + Regelrett
diff --git a/frontend/beCompliant/package-lock.json b/frontend/beCompliant/package-lock.json index 6c0203bf2..00ba44fbc 100644 --- a/frontend/beCompliant/package-lock.json +++ b/frontend/beCompliant/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@kvib/react": "^4.3.0", "framer-motion": "^11.2.6", + "material-symbols": "^0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.23.1" @@ -2373,6 +2374,12 @@ "react-dom": "^18.2.0" } }, + "node_modules/@kvib/react/node_modules/material-symbols": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.17.4.tgz", + "integrity": "sha512-5zI+rSzAidMJxAIrQCVwnp4rMjFnx8aQg68lfFXtaDeksZzJ7m8eDl16y9bRNxMosuYbLKeDHDbOWHPJJTSLhQ==", + "license": "Apache-2.0" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5675,9 +5682,10 @@ } }, "node_modules/material-symbols": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.17.4.tgz", - "integrity": "sha512-5zI+rSzAidMJxAIrQCVwnp4rMjFnx8aQg68lfFXtaDeksZzJ7m8eDl16y9bRNxMosuYbLKeDHDbOWHPJJTSLhQ==" + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.20.0.tgz", + "integrity": "sha512-bTxLbVDimYqe1+iY0qcWXJSqoxKfQhzfdVLlvi8DegHAuSJ93oXhMjHY95zOVcgKUsLjszR2heIYhLWjobE1BQ==", + "license": "Apache-2.0" }, "node_modules/memoize-one": { "version": "6.0.0", diff --git a/frontend/beCompliant/package.json b/frontend/beCompliant/package.json index 32172a9c2..16f03aaec 100644 --- a/frontend/beCompliant/package.json +++ b/frontend/beCompliant/package.json @@ -13,6 +13,7 @@ "dependencies": { "@kvib/react": "^4.3.0", "framer-motion": "^11.2.6", + "material-symbols": "^0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.23.1" diff --git a/frontend/beCompliant/src/components/MobileFilter.tsx b/frontend/beCompliant/src/components/MobileFilter.tsx index a43305de9..97ef3b576 100644 --- a/frontend/beCompliant/src/components/MobileFilter.tsx +++ b/frontend/beCompliant/src/components/MobileFilter.tsx @@ -68,7 +68,7 @@ const MobileFilter = ({ {tableMetadata?.fields.map((metaColumn, index) => ( >; team?: string; tableFilterProps: TableFilterProps; @@ -84,6 +84,7 @@ const MobileTableView = ({ record={item} setFetchNewAnswers={setFetchNewAnswers} team={team} + key={item.fields?.ID} /> diff --git a/frontend/beCompliant/src/components/answer/Answer.tsx b/frontend/beCompliant/src/components/answer/Answer.tsx index a8e8979fd..642092900 100644 --- a/frontend/beCompliant/src/components/answer/Answer.tsx +++ b/frontend/beCompliant/src/components/answer/Answer.tsx @@ -1,19 +1,24 @@ import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import { Select, useToast } from '@kvib/react'; +import { Select, useToast,Textarea, Button, Popover, PopoverTrigger, Box, PopoverBody, PopoverContent, PopoverArrow, PopoverCloseButton, PopoverHeader, useOutsideClick, useDisclosure, IconButton} from '@kvib/react'; import { RecordType } from '../../pages/Table'; +import { Choice } from '../../hooks/datafetcher'; +import colorUtils from '../../utils/colorUtils'; +import React from 'react'; export type AnswerType = { questionId: string; Svar: string; updated: string; + comment: string; }; interface AnswerProps { - choices: string[] | []; + choices: Choice[] | []; answer: string; record: RecordType; setFetchNewAnswers: Dispatch>; team?: string; + comment?: string; } export const Answer = ({ @@ -22,10 +27,19 @@ export const Answer = ({ record, setFetchNewAnswers, team, + comment, }: AnswerProps) => { const [selectedAnswer, setSelectedAnswer] = useState( answer ); + const [selectedComment, setComment] = useState(comment) + const [commentIsOpen, setCommentIsOpen] = useState(comment !== "") + + const backgroundColor = selectedAnswer + ? colorUtils.getHexForColor( + choices.find((choice) => choice.name === selectedAnswer)!.color + ) ?? undefined + : undefined; const toast = useToast(); @@ -33,7 +47,7 @@ export const Answer = ({ setSelectedAnswer(answer); }, [choices, answer]); - const submitAnswer = async (answer: string, record: RecordType) => { + const submitAnswer = async ( record: RecordType, answer?: string) => { const url = 'http://localhost:8080/answer'; // TODO: Place dev url to .env file const settings = { method: 'POST', @@ -56,45 +70,109 @@ export const Answer = ({ } toast({ title: 'Suksess', - description: "Svaret ditt er lagret", + description: 'Svaret ditt er lagret', status: 'success', duration: 5000, isClosable: true, - }) - + }); setFetchNewAnswers(true); } catch (error) { console.error('There was an error with the submitAnswer request:', error); toast({ title: 'Å nei!', - description: "Det har skjedd en feil. Prøv på nytt", + description: 'Det har skjedd en feil. Prøv på nytt', status: 'error', duration: 5000, isClosable: true, - }) + }); } return; }; - const handleChange = (e: React.ChangeEvent) => { + const submitComment = async (record: RecordType, comment?: string) => { + const url = 'http://localhost:8080/comments' + const settings = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + actor: 'Unknown', + questionId: record.fields.ID, + team: team, + comment: comment, + updated: '', + }), + }; + try { + const response = await fetch(url, settings); + if (!response.ok) { + throw new Error(`Error: ${response.status} ${response.statusText}`); + } + } + catch (error) { + console.error("Error submitting comment", error) } +} + + const handleAnswer = (e: React.ChangeEvent) => { const newAnswer: string = e.target.value; - setSelectedAnswer(newAnswer); - submitAnswer(newAnswer, record); + if (newAnswer.length > 0) { + setSelectedAnswer(newAnswer); + submitAnswer(record, newAnswer); + } }; + const handleCommentSubmit = () => { + console.log(selectedComment) + if(selectedComment !== comment) { + submitComment(record, selectedComment) + } + } + + const handleCommentState = (e: React.ChangeEvent) => { + setComment(e.target.value) + } + + + return ( - - {choices.map((choice, index) => ( - ))} + + {commentIsOpen && <> +