diff --git a/frontend/src/admin/project-form.tsx b/frontend/src/admin/project-form.tsx index b25251d6..59d6de9a 100644 --- a/frontend/src/admin/project-form.tsx +++ b/frontend/src/admin/project-form.tsx @@ -84,8 +84,7 @@ export const ProjectForm: FunctionComponent = () => { if (response.ok) { navigate(`/projects`); } else { - const errorData = await response.json(); - alert(`Error: ${errorData.message}`); + alert(`Error fetching project: ${response.statusText}`); } } finally { setIsEditing(false); diff --git a/frontend/src/admin/use-users.ts b/frontend/src/admin/use-users.ts new file mode 100644 index 00000000..ac7ab132 --- /dev/null +++ b/frontend/src/admin/use-users.ts @@ -0,0 +1,55 @@ +import {useState} from "react"; +import {useOnce} from "../hooks/use-once"; +import {User, usersFromJson } from "zero-zummon" + +type UseUserReturn = { + loadingUsers: boolean, + users: User[], + changeUser: (newUser: User) => void, + removeUser: (userId: string) => void, +} + +export const useUsers = (): UseUserReturn => { + const [loadingUsers, setLoading] = useState(true) + const [users, setUsers] = useState([]) + + const changeUser = (newUser: User) => { + setUsers(users.map(user => user.id.toString() === newUser.id.toString() ? newUser : user)) + } + + useOnce(async () => { + try { + const response = await fetch(import.meta.env.VITE_ZTOR_URL + '/users', { + credentials: 'include', + }) + if (response.status === 401) { + redirectToLogin() + return + } + if (!response.ok) { + throw new Error(`Could not load users: ${response.status} ${response.statusText}`) + } + + setUsers(usersFromJson(await response.text())) + } catch (error) { + alert((error as Error).message) + } finally { + setLoading(false) + } + }) + + const removeUser = (userId: any) => { + setUsers(users.filter(user => user.id.toString() !== userId.toString())) + } + + return { + loadingUsers, + users, + changeUser, + removeUser, + } +} + +export const redirectToLogin = () => { + window.location.href = import.meta.env.VITE_ZTOR_URL + '/login?redirectUrl=' + encodeURIComponent(window.location.href) +} diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx new file mode 100644 index 00000000..98672e58 --- /dev/null +++ b/frontend/src/admin/user-form.tsx @@ -0,0 +1,148 @@ +import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { PrimeReactProvider } from "primereact/api"; +import { InputText } from "primereact/inputtext"; +import { Button } from "primereact/button"; +import { User } from "zero-zummon"; +import { redirectToLogin } from "./use-users"; + +export const UserForm: FunctionComponent = () => { + const {userId} = useParams<{ userId: string }>(); + const [user, setUser] = useState(null); + const [originalData, setOriginalData] = useState(null); + + const [loading, setLoading] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const navigate = useNavigate(); + + const handleCancel = () => { + if (originalData) { + setUser(originalData); // Revert to original data + } + setIsEditing(false); + }; + + const handleEditToggle = () => { + setIsEditing(true); + }; + + const handleInputChange =(e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setUser((prev) => ({...prev, + [name]: type === "checkbox" ? checked : value, + } as User)); + }; + + useEffect(() => { + if (userId) { + const fetchUser = async () => { + setLoading(true); + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}`, { + credentials: "include", + }); + if (response.status === 401) { + redirectToLogin(); + return; + } + if (response.ok) { + const userData = await response.json(); + setUser(userData); + setOriginalData(userData); + } else { + alert(`Error fetching user: ${response.statusText}`); + } + } catch (error) { + alert((error as Error).message); + } finally { + setLoading(false); + } + }; + fetchUser(); + } else { + setIsEditing(true); + } + }, [userId]); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + try { + const method = userId ? "PUT" : "POST"; + const url = `${import.meta.env.VITE_ZTOR_URL}/users` + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(user), + }); + if (response.status === 401) { + redirectToLogin(); + return; + } + if (!response.ok) { + alert(`Error: ${response.statusText}`); + } + + navigate(`/users`); + } finally { + setIsEditing(false); + setLoading(false); + } + }; + + return ( + +
+

{userId ? "Edit User" : "Add User"}

+
+ + + + +
+ +
+ +
+ {isEditing ? ( + <> +
+ +
+
+ ); +}; diff --git a/frontend/src/admin/users.tsx b/frontend/src/admin/users.tsx new file mode 100644 index 00000000..e19b1a58 --- /dev/null +++ b/frontend/src/admin/users.tsx @@ -0,0 +1,70 @@ +import React, {FunctionComponent} from "react"; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import {useUsers} from "./use-users"; +import {PrimeReactProvider} from "primereact/api"; +import {User} from "zero-zummon" + +import "primereact/resources/themes/lara-light-cyan/theme.css" +import 'primeicons/primeicons.css' +import {DeleteButton} from "./delete-button"; +import {EditButton} from "./edit-button"; +import {Button} from "primereact/button"; +import {useNavigate} from "react-router-dom" + +export const Users: FunctionComponent = () => { + const {loadingUsers, users, changeUser, removeUser} = useUsers() + const navigate = useNavigate(); + + return ( + +
+

Users List

+
+ + + ( +
+ {user.isAdmin ? ( + + ) : ( + + )} +
+ )} + /> + ( +
*': { + margin: `${1 / 6}rem` + }, + }}> + + +
+ )}/> +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/company-survey-v2/survey.tsx b/frontend/src/components/company-survey-v2/survey.tsx index 2276b531..34924d4f 100644 --- a/frontend/src/components/company-survey-v2/survey.tsx +++ b/frontend/src/components/company-survey-v2/survey.tsx @@ -105,7 +105,7 @@ const SurveyWithReset: FunctionComponent<{ } useEffect(() => { - const subscription = watch((value, { name, type }) => + const subscription = watch((value, {name, type}) => localStorage.setItem(localStorageKey, JSON.stringify(value)) ) return () => subscription.unsubscribe() @@ -126,6 +126,7 @@ const SurveyWithReset: FunctionComponent<{ try { const response = await fetch(url, { method: 'POST', + credentials: "include", headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -144,6 +145,7 @@ const SurveyWithReset: FunctionComponent<{ return } + localStorage.removeItem(localStorageKey) navigate('/bedankt', { state: { deeplink: await response.json() @@ -302,7 +304,7 @@ const loadFromLocalStorage = (localStorageKey: string): any => { return null } - const previous = JSON.parse(previousData) + let previous = JSON.parse(previousData) for (const tab of previous.tabs) { // these fields were renamed and are now unknown in the back-end @@ -313,7 +315,8 @@ const loadFromLocalStorage = (localStorageKey: string): any => { try { const prepared = prepareForSubmit(previous, 'Testproject') const str = JSON.stringify(prepared) - const surveyObject = surveyFromJson(str) + const surveyObject = surveyFromJson(str).clearIds() + previous = surveyToFormData(JSON.parse(surveyObject.toPrettyJson())) } catch (e) { // The goal is to prevent the user from getting stuck due to schema changes by Zenmo. const shouldContinue = confirm(`Eerder ingevulde gegevens bevatten een fout of zijn niet compleet. diff --git a/frontend/src/components/company-survey-v2/time-series/interval-dropdown.tsx b/frontend/src/components/company-survey-v2/time-series/interval-dropdown.tsx new file mode 100644 index 00000000..e15fa8b8 --- /dev/null +++ b/frontend/src/components/company-survey-v2/time-series/interval-dropdown.tsx @@ -0,0 +1,29 @@ +import {FunctionComponent} from "react" +import {Dropdown} from "primereact/dropdown" +import {isoStringToDateTimeUnit, dateTimeUnitToIsoString} from "zero-zummon" +import {SelectItem, SelectItemOptionsType} from "primereact/selectitem" + +const options: SelectItemOptionsType = [ + { + value: "PT15M", + label: "Kwartier", + }, + { + value: "P1D", + label: "Dag", + }, + { + value: "P1M", + label: "Maand", + }, +] + +export const IntervalDropdown: FunctionComponent<{ + timeStep: any, + setTimeStep: (timeStep: any) => void +}> = ({timeStep, setTimeStep}) => { + + return ( + setTimeStep(isoStringToDateTimeUnit(event.value))}/> + ) +} diff --git a/frontend/src/components/company-survey-v2/time-series/time-series-textarea.tsx b/frontend/src/components/company-survey-v2/time-series/time-series-textarea.tsx index cd50c3d5..2c8e9230 100644 --- a/frontend/src/components/company-survey-v2/time-series/time-series-textarea.tsx +++ b/frontend/src/components/company-survey-v2/time-series/time-series-textarea.tsx @@ -7,6 +7,7 @@ import {LabelRow} from "../generic/label-row" import {displayTimeZone, kotlinInstantToJsJodaInstant, prettyPrint} from "./time-series-util" import {Dropdown} from "primereact/dropdown" import {InputText} from "primereact/inputtext" +import {IntervalDropdown} from "./interval-dropdown" const placeholder = ` bijvoorbeeld: @@ -50,6 +51,9 @@ export const TimeSeriesTextarea: FunctionComponent<{timeSeries: TimeSeries, setT + + setTimeSeriesImpl(internalTimeSeries.withTimeStep(timeStep))} /> + = () => { Projects + {/* loadContent('/users')} css={buttonStyle}> + + Users + */} loadContent('/simulation')} css={buttonStyle}> Simulation diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 24b7bc29..ea8760d1 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -7,7 +7,9 @@ import {LoginWidget} from "./user/login"; import {BedrijvenFormV1} from "./components/bedrijven-form-v1"; import {Surveys} from "./admin/surveys"; import {Projects} from "./admin/projects"; +import {Users} from "./admin/users"; import {ProjectForm} from "./admin/project-form"; +import {UserForm} from "./admin/user-form"; import {fetchSurveyById, SurveyById, SurveyByIdLoaderData} from "./components/company-survey-v2/survey-by-id" import {Intro} from "./components/intro" import {ExcelImport} from "./excel-import/excel-import" @@ -26,6 +28,9 @@ export const router = createBrowserRouter([ {path: "/projects", element: }, {path: "/projects/new-project", element: }, {path: "/projects/:projectId", element: }, + {path: "/users", element: }, + {path: "/users/new-user", element: }, + {path: "/users/:userId", element: }, {path: "/simulation", element: }, ], }, diff --git a/fudura-client/src/main/kotlin/GetChannelMetadataResult.kt b/fudura-client/src/main/kotlin/GetChannelMetadataResult.kt index 5b8291a7..b82bce9d 100644 --- a/fudura-client/src/main/kotlin/GetChannelMetadataResult.kt +++ b/fudura-client/src/main/kotlin/GetChannelMetadataResult.kt @@ -9,6 +9,7 @@ data class GetChannelMetadataResult( val channelType: ChannelType, val channelDataType: ChannelDataType, val productType: ProductType, + // for cumulative gas values, this will contain the word "standen" val description: String, val longDescription: String, // "00:15:00" for quarter-hourly data. @@ -25,6 +26,7 @@ data class GetChannelMetadataResult( enum class ChannelType { Profiel, Register, // Used for maandmaximum, maandtotaal + Onbekend, } enum class ChannelDataType { @@ -39,13 +41,14 @@ enum class UnitOfMeasurement { kVarh, kVAr, m3, + K, // Hoogcalorisch gas? Onbekend, } enum class Direction { Consumption, Production, - Unknown, + Unknown, // is often Consumption - Production } enum class ProductType { diff --git a/migrations/V33__admin_flag_to_user.sql b/migrations/V33__admin_flag_to_user.sql new file mode 100644 index 00000000..fdf671eb --- /dev/null +++ b/migrations/V33__admin_flag_to_user.sql @@ -0,0 +1 @@ +ALTER TABLE "user" ADD COLUMN is_admin BOOLEAN DEFAULT FALSE; diff --git a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt index e7545eed..642dc0ef 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt @@ -8,6 +8,8 @@ import com.zenmo.orm.user.table.UserTable import com.zenmo.zummon.companysurvey.* import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList +import org.jetbrains.exposed.sql.SqlExpressionBuilder.notInList import org.jetbrains.exposed.sql.transactions.transaction import java.util.* import kotlin.uuid.ExperimentalUuidApi @@ -571,6 +573,23 @@ class SurveyRepository( this[GridConnectionTable.thermalStorageKw] = gridConnection.storage.thermalStorageKw } + // if the survey has grid connections in the database which are not in the data object, remove those. + GridConnectionTable.deleteWhere { + GridConnectionTable.id.notInList(survey.gridConnectionIds()) + .and(GridConnectionTable.addressId eq anyFrom ( + AddressTable.select(AddressTable.id).where { + AddressTable.surveyId eq surveyId + } + ) + ) + } + + // if the survey has address in the database which are not in the data object, remove those. + AddressTable.deleteWhere { + AddressTable.id.notInList(survey.addresses.map { it.id }) + .and(AddressTable.id.inList(listOf(surveyId))) + } + for (address in survey.addresses) { for (gridConnection in address.gridConnections) { for (electricityFile in gridConnection.electricity.quarterHourlyValuesFiles) { diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt deleted file mode 100644 index 918dc2a7..00000000 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.zenmo.orm.user - -import com.zenmo.zummon.companysurvey.Project -import java.util.UUID - -data class User( - // Keycloak id - val id: UUID, - val projects: List = emptyList(), - val note: String, -) diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt index 42d25f73..16bf213e 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt @@ -1,5 +1,6 @@ package com.zenmo.orm.user +import com.zenmo.zummon.User import com.zenmo.orm.companysurvey.ProjectRepository import com.zenmo.orm.user.table.UserProjectTable import com.zenmo.orm.user.table.UserTable @@ -10,6 +11,8 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction import java.util.UUID import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toJavaUuid +import kotlin.uuid.toKotlinUuid class UserRepository( private val db: Database, @@ -53,10 +56,25 @@ class UserRepository( } } - fun getUserById(id: UUID): User? { + fun getUserById(id: UUID): User { return getUsers( (UserTable.id eq id) - ).firstOrNull() + ).first() + } + + @OptIn(ExperimentalUuidApi::class) + fun save( + user: User, + ) { + transaction(db) { + UserTable.upsertReturning() { + it[id] = user.id.toJavaUuid() + it[UserTable.note] = user.note + it[UserTable.isAdmin] = user.isAdmin + }.map { + hydrateUser(it) + }.first() + } } fun saveUser( @@ -88,10 +106,16 @@ class UserRepository( } } + fun isAdmin(userId: UUID): Boolean { + val user = getUserById(userId) + return user?.isAdmin ?: false + } + protected fun hydrateUser(row: ResultRow): User { return User( - id = row[UserTable.id], + id = row[UserTable.id].toKotlinUuid(), note = row[UserTable.note], + isAdmin = row[UserTable.isAdmin], projects = emptyList(), // data from different table ) } diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/table/User.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/table/User.kt index 3d032a84..e44b608b 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/table/User.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/table/User.kt @@ -16,4 +16,9 @@ object UserTable: Table("user") { * This is mostly because there is no GUI yet and no logic to display the name from Keycloak. */ val note = varchar("note", 255).default("") + + /** + * Admin flag to determine if the user has admin privileges. + */ + val isAdmin = bool("is_admin").default(false) } diff --git a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt index c6cfc5fc..ed33ed65 100644 --- a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt +++ b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt @@ -4,6 +4,8 @@ import com.zenmo.orm.cleanDb import com.zenmo.orm.companysurvey.table.CompanySurveyTable import com.zenmo.orm.connectToPostgres import com.zenmo.orm.user.UserRepository +import com.zenmo.zummon.companysurvey.Address +import com.zenmo.zummon.companysurvey.GridConnection import com.zenmo.zummon.companysurvey.Survey import com.zenmo.zummon.companysurvey.toDuration import org.jetbrains.exposed.sql.Database @@ -61,6 +63,38 @@ class SurveyRepositoryTest { assertEquals(1, gasTimeStep.toDuration().inWholeHours) } + @Test + fun testRemoveOneOfTwoGridConnections() { + val projectName = "WessenHoort" + projectRepository.saveNewProject(projectName) + val survey = createMockSurvey(projectName) + val address = survey.addresses.single() + val surveyWithTwoGridConnections = survey.copy( + addresses = listOf( + address.copy( + gridConnections = address.gridConnections + GridConnection() + ) + ) + ) + + surveyRepository.save(surveyWithTwoGridConnections) + val storedSurvey = surveyRepository.getSurveyById(survey.id)!! + assertEquals(2, storedSurvey.numGridConnections) + val surveyWithFirstGc = storedSurvey.copy( + addresses = listOf( + storedSurvey.addresses.single().copy( + gridConnections = storedSurvey.addresses.single().gridConnections.take(1) + ) + ) + ) + surveyRepository.save(surveyWithFirstGc) + + val storedSurveyWithFirstGc = surveyRepository.getSurveyById(survey.id)!! + assertEquals(1, storedSurveyWithFirstGc.numGridConnections) + assertEquals(storedSurvey.flattenedGridConnections().first().id, storedSurveyWithFirstGc.getSingleGridConnection().id) + + } + @Test fun testUserCantAccessSurveyInOtherProject() { val ownProjectName = "Middelkaap" diff --git a/zorm/src/test/kotlin/com/zenmo/orm/user/UserRepositoryTest.kt b/zorm/src/test/kotlin/com/zenmo/orm/user/UserRepositoryTest.kt index a767e945..011c2a5f 100644 --- a/zorm/src/test/kotlin/com/zenmo/orm/user/UserRepositoryTest.kt +++ b/zorm/src/test/kotlin/com/zenmo/orm/user/UserRepositoryTest.kt @@ -10,7 +10,7 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction import java.util.UUID import kotlin.test.* -import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid class UserRepositoryTest { val db: Database = connectToPostgres() @@ -29,7 +29,7 @@ class UserRepositoryTest { userRepository.saveUser(userId, note = note) val result = userRepository.getUserById(userId) assertNotNull(result) - assertEquals(userId, result.id) + assertEquals(userId.toKotlinUuid(), result.id) assertEquals(note, result.note) } @@ -40,7 +40,7 @@ class UserRepositoryTest { val result = userRepository.getUserById(userId) assertNotNull(result) - assertEquals(userId, result.id) + assertEquals(userId.toKotlinUuid(), result.id) assertEquals("", result.note) } @@ -90,7 +90,7 @@ class UserRepositoryTest { } val users = userRepository.getUsers((UserTable.note eq "Specific user note")) assertEquals(1, users.size) - assertEquals(userId, users.first().id) + assertEquals(userId.toKotlinUuid(), users.first().id) } @Test @@ -108,7 +108,7 @@ class UserRepositoryTest { // Assertions assertNotNull(user) - assertEquals(userId, user.id) + assertEquals(userId.toKotlinUuid(), user.id) assertEquals("Test Note", user.note) assertEquals(2, user.projects.size) @@ -129,11 +129,11 @@ class UserRepositoryTest { // Retrieve users val users = userRepository.getUsersAndProjects() - val user = users.find { it.id == userId } + val user = users.find { it.id == userId.toKotlinUuid() } // Assertions assertNotNull(user) - assertEquals(userId, user?.id) + assertEquals(userId.toKotlinUuid(), user?.id) assertEquals("User without projects", user?.note) assertTrue(user?.projects?.isEmpty() == true) } @@ -148,8 +148,9 @@ class UserRepositoryTest { assertNotNull(userBeforeDelete) userRepository.deleteUserById(userId) - val userAfterDelete = userRepository.getUserById(userId) - assertTrue(userAfterDelete == null) + assertFailsWith(NoSuchElementException::class) { + userRepository.getUserById(userId) + } } @Test diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 0a437bc8..5848cd14 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -10,6 +10,9 @@ import com.zenmo.ztor.errorMessageToJson import com.zenmo.ztor.user.getUserId import com.zenmo.zummon.companysurvey.Survey import com.zenmo.zummon.companysurvey.Project +import com.zenmo.zummon.User +import com.zenmo.zummon.usersFromJson + import io.ktor.http.* import io.ktor.serialization.* import io.ktor.server.application.* @@ -28,7 +31,83 @@ fun Application.configureDatabases(): Database { val deeplinkService = DeeplinkService(DeeplinkRepository(db)) routing { - // List projects for current user + suspend fun RoutingContext.asAdmin(body: suspend () -> Unit) { + val userId = call.getUserId() + + if (userId == null) { + call.respond(HttpStatusCode.Unauthorized, "User not logged in") + return + } + + if (!userRepository.isAdmin(userId)) { + call.respond(HttpStatusCode.Forbidden, "User is not admin") + return + } + + body() + } + + get("/users") { + asAdmin { + val users = userRepository.getUsers() + call.respond(HttpStatusCode.OK, users) + } + } + + get("/users/{userId}") { + asAdmin { + val userId = UUID.fromString(call.parameters["userId"]) + val user = userRepository.getUserById(userId) + call.respond(HttpStatusCode.OK, user) + } + } + + // Create + post("/users") { + val user: User? + try { + user = call.receive() + } catch (e: BadRequestException) { + if (e.cause is JsonConvertException) { + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.cause?.message)) + return@post + } + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.message)) + return@post + } + asAdmin { + val newUser = userRepository.save(user) + call.respond(HttpStatusCode.Created, newUser) + } + } + + // Update + put("/users") { + val user: User? + try { + user = call.receive() + } catch (e: BadRequestException) { + if (e.cause is JsonConvertException) { + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.cause?.message)) + return@put + } + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.message)) + return@put + } + asAdmin { + val newUser = userRepository.save(user) + call.respond(HttpStatusCode.OK, newUser) + } + } + + delete("/users/{userId}") { + asAdmin { + val userId = UUID.fromString(call.parameters["userId"]) + userRepository.deleteUserById(userId) + call.respond(HttpStatusCode.OK) + } + } + get("/projects") { val userId = call.getUserId() if (userId == null) { @@ -39,12 +118,10 @@ fun Application.configureDatabases(): Database { call.respond(HttpStatusCode.OK, projectRepository.getProjectsByUserId(userId)) } - // Get one project that belongs to the user get("/projects/{projectId}") { val projectId = UUID.fromString(call.parameters["projectId"]) val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@get @@ -68,7 +145,6 @@ fun Application.configureDatabases(): Database { } val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@post @@ -182,7 +258,6 @@ fun Application.configureDatabases(): Database { delete("/company-surveys/{surveyId}") { val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@delete @@ -220,7 +295,6 @@ fun Application.configureDatabases(): Database { // set active state put("/company-surveys/{surveyId}/include-in-simulation") { val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@put @@ -233,6 +307,5 @@ fun Application.configureDatabases(): Database { call.respond(HttpStatusCode.OK) } } - return db } diff --git a/zummon/src/commonMain/kotlin/User.kt b/zummon/src/commonMain/kotlin/User.kt index cc882379..6bf576c8 100644 --- a/zummon/src/commonMain/kotlin/User.kt +++ b/zummon/src/commonMain/kotlin/User.kt @@ -14,5 +14,16 @@ import kotlin.uuid.Uuid data class User( val id: Uuid = Uuid.random(), val note: String, - val projects: List = emptyList() + val projects: List = emptyList(), + val isAdmin: Boolean = false ) + +@JsExport +fun usersFromJson(json: String): Array { + return kotlinx.serialization.json.Json.decodeFromString>(json) +} + +@JsExport +fun userFromJson(json: String): User { + return kotlinx.serialization.json.Json.decodeFromString(json) +} diff --git a/zummon/src/commonMain/kotlin/companysurvey/Address.kt b/zummon/src/commonMain/kotlin/companysurvey/Address.kt index 65eb1a0f..210790de 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Address.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Address.kt @@ -26,4 +26,9 @@ data class Address( */ public val gridConnectionArray: Array get() = gridConnections.toTypedArray() + + fun clearIds() = copy( + id = uuid4(), + gridConnections = gridConnections.map { it.clearId() } + ) } diff --git a/zummon/src/commonMain/kotlin/companysurvey/GridConnection.kt b/zummon/src/commonMain/kotlin/companysurvey/GridConnection.kt index a2db73dc..57533293 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/GridConnection.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/GridConnection.kt @@ -30,4 +30,6 @@ data class GridConnection( val expansionPlans: String = "", val electrificationPlans: String = "", val surveyFeedback: String = "", -) +) { + fun clearId() = copy(id = uuid4()) +} diff --git a/zummon/src/commonMain/kotlin/companysurvey/Survey.kt b/zummon/src/commonMain/kotlin/companysurvey/Survey.kt index cb205423..2d2dd109 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Survey.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Survey.kt @@ -46,6 +46,8 @@ data class Survey( } } + fun gridConnectionIds(): List = flattenedGridConnections().map { it.id } + /** * For sorting in JavaScript primereact/datatable */ @@ -161,6 +163,16 @@ data class Survey( } return prettyJson.encodeToString(Survey.serializer(), this) } + + /** + * When you want to create a new Survey from an existing one. + */ + fun clearIds() = copy( + id = uuid4(), + createdAt = Clock.System.now().roundToMilliseconds(), + addresses = addresses.map { it.clearIds() }, + createdBy = null, + ) } @JsExport