From 28fda62443257fafa40f66d7cdfd6b26c6f90e61 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Sun, 15 Dec 2024 12:44:17 +0000 Subject: [PATCH 1/9] make buttons generic --- frontend/src/admin/delete-button.tsx | 6 +++--- frontend/src/admin/edit-button.tsx | 4 ++-- frontend/src/admin/surveys.tsx | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/admin/delete-button.tsx b/frontend/src/admin/delete-button.tsx index dbfaeb2c..81a00bbc 100644 --- a/frontend/src/admin/delete-button.tsx +++ b/frontend/src/admin/delete-button.tsx @@ -2,7 +2,7 @@ import {Button} from "primereact/button"; import {FunctionComponent, useState} from "react"; import {noop} from "lodash"; -export const DeleteButton: FunctionComponent<{surveyId: any, onDelete?: (surveyId: any) => void}> = ({surveyId, onDelete = noop}) => { +export const DeleteButton: FunctionComponent<{type: string, id: any, onDelete?: (type: string , id: any) => void}> = ({type, id, onDelete = noop}) => { const [pending, setPending] = useState(false) const deleteSurvey = async () => { setPending(true) @@ -10,11 +10,11 @@ export const DeleteButton: FunctionComponent<{surveyId: any, onDelete?: (surveyI if (!confirm('Uitvraag verwijderen?')) { return } - await fetch(`${import.meta.env.VITE_ZTOR_URL}/company-surveys/${surveyId}`, { + await fetch(`${import.meta.env.VITE_ZTOR_URL}/${type}/${id}`, { method: 'DELETE', credentials: 'include', }) - onDelete(surveyId) + onDelete(id) } catch (error) { alert((error as Error).message) } finally { diff --git a/frontend/src/admin/edit-button.tsx b/frontend/src/admin/edit-button.tsx index d7250459..49c3d7b2 100644 --- a/frontend/src/admin/edit-button.tsx +++ b/frontend/src/admin/edit-button.tsx @@ -1,7 +1,7 @@ import {Link} from "react-router-dom"; -export const EditButton = ({surveyId}: {surveyId: string}) => ( - ( + diff --git a/frontend/src/admin/surveys.tsx b/frontend/src/admin/surveys.tsx index e693a1bb..dd39d9fd 100644 --- a/frontend/src/admin/surveys.tsx +++ b/frontend/src/admin/surveys.tsx @@ -1,4 +1,4 @@ -import {FunctionComponent} from "react"; +import React, {FunctionComponent} from "react"; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import {useSurveys} from "./use-surveys"; @@ -70,8 +70,8 @@ export const Surveys: FunctionComponent = () => { }, }}> - - + + )}/> From 75b9a63740cb06f578372d1fe960c27e0d8031ee Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Sun, 15 Dec 2024 12:47:01 +0000 Subject: [PATCH 2/9] List projects --- frontend/src/admin/projects.tsx | 59 +++++++++++++++++++ frontend/src/admin/use-projects.ts | 52 ++++++++++++++++ .../{project.ts => project.tsx} | 11 ++++ frontend/src/router.tsx | 2 + .../orm/companysurvey/ProjectRepository.kt | 13 ++-- .../com/zenmo/orm/dbutil/GenerateUuid.kt | 1 + .../com/zenmo/ztor/plugins/Databases.kt | 2 +- .../kotlin/companysurvey/Project.kt | 10 +++- 8 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 frontend/src/admin/projects.tsx create mode 100644 frontend/src/admin/use-projects.ts rename frontend/src/components/company-survey-v2/{project.ts => project.tsx} (79%) diff --git a/frontend/src/admin/projects.tsx b/frontend/src/admin/projects.tsx new file mode 100644 index 00000000..90a817f3 --- /dev/null +++ b/frontend/src/admin/projects.tsx @@ -0,0 +1,59 @@ +import React, {FunctionComponent} from "react"; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import {useProjects} from "./use-projects"; +import {PrimeReactProvider} from "primereact/api"; +import {Project} 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 Projects: FunctionComponent = () => { + const {loadingProjects, projects, changeProject, removeProject} = useProjects() + const navigate = useNavigate(); + + return ( + +
+

Projects List

+
+ + + + + ( +
*': { + margin: `${1 / 6}rem` + }, + }}> + + +
+ )}/> +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/admin/use-projects.ts b/frontend/src/admin/use-projects.ts new file mode 100644 index 00000000..31bfdab4 --- /dev/null +++ b/frontend/src/admin/use-projects.ts @@ -0,0 +1,52 @@ +import {useState} from "react"; +import {useOnce} from "../hooks/use-once"; +import {Project, projectsFromJson } from "zero-zummon" + +type UseProjectReturn = { + loadingProjects: boolean, + projects: Project[], + changeProject: (newProject: Project) => void, + removeProject: (projectId: string) => void, +} + +export const useProjects = (): UseProjectReturn => { + const [loadingProjects, setLoading] = useState(true) + const [projects, setProjects] = useState([]) + + const changeProject = (newProject: Project) => { + setProjects(projects.map(project => project.id.toString() === newProject.id.toString() ? newProject : project)) + } + + useOnce(async () => { + try { + const response = await fetch(import.meta.env.VITE_ZTOR_URL + '/projects', { + credentials: 'include', + }) + if (response.status === 401) { + redirectToLogin() + return + } + + setProjects(projectsFromJson(await response.text())) + } catch (error) { + alert((error as Error).message) + } finally { + setLoading(false) + } + }) + + const removeProject = (projectId: any) => { + setProjects(projects.filter(project => project.id.toString() !== projectId.toString())) + } + + return { + loadingProjects, + projects, + changeProject, + removeProject, + } +} + +export const redirectToLogin = () => { + window.location.href = import.meta.env.VITE_ZTOR_URL + '/login?redirectUrl=' + encodeURIComponent(window.location.href) +} \ No newline at end of file diff --git a/frontend/src/components/company-survey-v2/project.ts b/frontend/src/components/company-survey-v2/project.tsx similarity index 79% rename from frontend/src/components/company-survey-v2/project.ts rename to frontend/src/components/company-survey-v2/project.tsx index 0a998360..d191fc37 100644 --- a/frontend/src/components/company-survey-v2/project.ts +++ b/frontend/src/components/company-survey-v2/project.tsx @@ -1,4 +1,6 @@ import {fetchBuurtcodesByProject} from "../../panden-select/fetch-buurtcodes" +import {ztorFetch} from "../../services/ztor-fetch"; +import {Project} from "zero-zummon" export type FrontendProjectConfiguration = { name: ProjectName, @@ -46,3 +48,12 @@ export async function getProjectConfiguration(projectName: ProjectName): Promise buurtcodes, } } + +export async function getProjectById(projectId: string): Promise { + try { + return await ztorFetch(`/projects/${projectId}`); + } catch (error) { + console.error(`An error occurred while fetching the project by ID: ${projectId}`, error); + return null; + } +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index e5104913..df267f8d 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -6,6 +6,7 @@ import {ThankYou} from './components/thank-you' import {LoginWidget} from "./user/login"; import {BedrijvenFormV1} from "./components/bedrijven-form-v1"; import {Surveys} from "./admin/surveys"; +import {Projects} from "./admin/projects"; import {fetchSurveyById, SurveyById, SurveyByIdLoaderData} from "./components/company-survey-v2/survey-by-id" import {Intro} from "./components/intro" import {ExcelImport} from "./excel-import/excel-import" @@ -21,6 +22,7 @@ export const router = createBrowserRouter([ children: [ {path: "", element: }, {path: "/surveys", element: }, + {path: "/projects", element: }, {path: "/simulation", element: }, ], }, diff --git a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt index 1270f70a..56c1f83b 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt @@ -7,6 +7,9 @@ import org.jetbrains.exposed.sql.* 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.Uuid +import kotlin.uuid.toKotlinUuid class ProjectRepository( val db: Database @@ -23,10 +26,10 @@ class ProjectRepository( } } - fun getProjectById(id: UUID): Project? { + fun getProjectById(id: UUID): Project { return getProjects( (ProjectTable.id eq id) - ).firstOrNull() + ).first() } fun getProjectsByUserId(userId: UUID): List = @@ -45,10 +48,11 @@ class ProjectRepository( } } + @OptIn(ExperimentalUuidApi::class) fun save(project: Project): Project { return transaction(db) { ProjectTable.upsertReturning() { - it[id] = project.id + it[id] = UUID.fromString(project.id.toString()) it[name] = project.name it[energiekeRegioId] = project.energiekeRegioId it[buurtCodes] = project.buurtCodes @@ -79,9 +83,10 @@ class ProjectRepository( .single()[ProjectTable.buurtCodes] } + @OptIn(ExperimentalUuidApi::class) fun hydrateProject(row: ResultRow): Project { return Project( - id = row[ProjectTable.id], + id = row[ProjectTable.id].toKotlinUuid(), name = row[ProjectTable.name], energiekeRegioId = row[ProjectTable.energiekeRegioId], buurtCodes = row[ProjectTable.buurtCodes] diff --git a/zorm/src/main/kotlin/com/zenmo/orm/dbutil/GenerateUuid.kt b/zorm/src/main/kotlin/com/zenmo/orm/dbutil/GenerateUuid.kt index 5e3155d3..394c24c1 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/dbutil/GenerateUuid.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/dbutil/GenerateUuid.kt @@ -5,6 +5,7 @@ import org.jetbrains.exposed.sql.CustomFunction import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.UUIDColumnType import java.util.UUID +import kotlin.uuid.Uuid val postgresGenRandomUuid = CustomFunction("gen_random_uuid", UUIDColumnType()) 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 dc920d6b..bfd49a97 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -27,7 +27,7 @@ fun Application.configureDatabases(): Database { routing { get("/projects") { val userId = call.getUserId() - if (userId == null) { + if (userId == null) { // Check if it's admin to return all the projects call.respond(HttpStatusCode.Unauthorized) return@get } diff --git a/zummon/src/commonMain/kotlin/companysurvey/Project.kt b/zummon/src/commonMain/kotlin/companysurvey/Project.kt index 70911c63..110fd045 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Project.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Project.kt @@ -1,10 +1,9 @@ package com.zenmo.zummon.companysurvey -import com.benasher44.uuid.Uuid -import com.benasher44.uuid.uuid4 import com.zenmo.zummon.BenasherUuidSerializer import kotlinx.serialization.Serializable import kotlin.js.JsExport +import kotlin.uuid.Uuid @Serializable @JsExport @@ -12,9 +11,14 @@ data class Project constructor( // @Contextual @Serializable(with = BenasherUuidSerializer::class) - val id: Uuid = uuid4(), + val id: Uuid, val name: String = "", // Project ID aka Energy Hub ID of Energieke Regio. val energiekeRegioId: Int?, val buurtCodes: List = emptyList(), ) + +@JsExport +fun projectsFromJson(json: String): Array { + return kotlinx.serialization.json.Json.decodeFromString>(json) +} From 81c15d224e04733c974a4b21607cceff5604df82 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 16 Dec 2024 15:01:33 +0000 Subject: [PATCH 3/9] Create and edit projects --- frontend/src/admin/project-form.tsx | 117 ++++++++++++++++++ frontend/src/admin/use-projects.ts | 53 +++++++- frontend/src/router.tsx | 3 + .../orm/companysurvey/ProjectRepository.kt | 2 +- .../com/zenmo/orm/dbutil/GenerateUuid.kt | 1 - .../com/zenmo/ztor/plugins/Databases.kt | 52 ++++++++ .../kotlin/companysurvey/Project.kt | 10 +- 7 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 frontend/src/admin/project-form.tsx diff --git a/frontend/src/admin/project-form.tsx b/frontend/src/admin/project-form.tsx new file mode 100644 index 00000000..f77436ec --- /dev/null +++ b/frontend/src/admin/project-form.tsx @@ -0,0 +1,117 @@ +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 { Project } from "zero-zummon"; // Assuming this is the project model +import { redirectToLogin } from "./use-projects"; + +export const ProjectForm: FunctionComponent = () => { + const { projectId } = useParams<{ projectId: string }>(); + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + if (projectId) { + const fetchProject = async () => { + setLoading(true); + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/projects/${projectId}`, { + credentials: "include", + }); + if (response.status === 401) { + redirectToLogin(); + return; + } + if (response.ok) { + const projectData = await response.json(); + setProject(projectData); + } else { + alert(`Error fetching project: ${response.statusText}`); + } + } catch (error) { + alert((error as Error).message); + } finally { + setLoading(false); + } + }; + fetchProject(); + } + }, [projectId]); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + try { + const method = projectId ? "PUT" : "POST"; + const url = projectId + ? `${import.meta.env.VITE_ZTOR_URL}/projects/${projectId}` + : `${import.meta.env.VITE_ZTOR_URL}/projects`; + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(project), + }); + if (response.status === 401) { + redirectToLogin(); + return; + } + if (response.ok) { + const projectData = await response.json(); + alert(`Project ${projectId ? "updated" : "created"} successfully!`); + navigate(`/projects/${projectData.id}`); + } else { + const errorData = await response.json(); + alert(`Error: ${errorData.message}`); + } + } catch (error) { + alert((error as Error).message); + } finally { + setLoading(false); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setProject((prev) => ({ ...prev, [name]: value } as Project)); + }; + + const groupStyle: React.CSSProperties = { + display: "flex", + flexDirection: "column", + gap: "1rem", + maxWidth: "400px", + margin: "0 auto", + }; + + return ( + +
+

{projectId ? "Edit Project" : "Add Project"}

+
+ + + + +
+
+ ); +}; diff --git a/frontend/src/admin/use-projects.ts b/frontend/src/admin/use-projects.ts index 31bfdab4..17350a48 100644 --- a/frontend/src/admin/use-projects.ts +++ b/frontend/src/admin/use-projects.ts @@ -1,6 +1,7 @@ import {useState} from "react"; import {useOnce} from "../hooks/use-once"; import {Project, projectsFromJson } from "zero-zummon" +import {useNavigate} from "react-router-dom"; type UseProjectReturn = { loadingProjects: boolean, @@ -9,6 +10,11 @@ type UseProjectReturn = { removeProject: (projectId: string) => void, } +type UseProjectData = { + loadingProject: boolean, + project: Project, +} + export const useProjects = (): UseProjectReturn => { const [loadingProjects, setLoading] = useState(true) const [projects, setProjects] = useState([]) @@ -26,6 +32,9 @@ export const useProjects = (): UseProjectReturn => { redirectToLogin() return } + if (response.status === 500) { + return + } setProjects(projectsFromJson(await response.text())) } catch (error) { @@ -47,6 +56,48 @@ export const useProjects = (): UseProjectReturn => { } } +export const useCreateProject = (): UseProjectData => { + const [loadingProject, setLoading] = useState(true) + const [project, setProject] = useState() + const navigate = useNavigate(); + + useOnce(async () => { + try { + const response = await fetch(import.meta.env.VITE_ZTOR_URL + '/projects', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(project), + }) + if (response.status === 401) { + redirectToLogin() + return + } + + if (response.ok) { + const createdProject = await response.json(); + alert(`Project created successfully with ID: ${createdProject.id}`); + setProject(createdProject) + navigate(`/projects/${createdProject.id}`); + } else { + const errorData = await response.json(); + alert(`Error creating project: ${errorData.message}`); + } + } catch (error) { + alert((error as Error).message) + } finally { + setLoading(false) + } + }) + + return { + loadingProject, + project, + } +} + export const redirectToLogin = () => { window.location.href = import.meta.env.VITE_ZTOR_URL + '/login?redirectUrl=' + encodeURIComponent(window.location.href) -} \ No newline at end of file +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index df267f8d..24b7bc29 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -7,6 +7,7 @@ import {LoginWidget} from "./user/login"; import {BedrijvenFormV1} from "./components/bedrijven-form-v1"; import {Surveys} from "./admin/surveys"; import {Projects} from "./admin/projects"; +import {ProjectForm} from "./admin/project-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" @@ -23,6 +24,8 @@ export const router = createBrowserRouter([ {path: "", element: }, {path: "/surveys", element: }, {path: "/projects", element: }, + {path: "/projects/new-project", element: }, + {path: "/projects/:projectId", element: }, {path: "/simulation", element: }, ], }, diff --git a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt index 56c1f83b..6a6da6de 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt @@ -86,7 +86,7 @@ class ProjectRepository( @OptIn(ExperimentalUuidApi::class) fun hydrateProject(row: ResultRow): Project { return Project( - id = row[ProjectTable.id].toKotlinUuid(), + id = row[ProjectTable.id], name = row[ProjectTable.name], energiekeRegioId = row[ProjectTable.energiekeRegioId], buurtCodes = row[ProjectTable.buurtCodes] diff --git a/zorm/src/main/kotlin/com/zenmo/orm/dbutil/GenerateUuid.kt b/zorm/src/main/kotlin/com/zenmo/orm/dbutil/GenerateUuid.kt index 394c24c1..5e3155d3 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/dbutil/GenerateUuid.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/dbutil/GenerateUuid.kt @@ -5,7 +5,6 @@ import org.jetbrains.exposed.sql.CustomFunction import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.UUIDColumnType import java.util.UUID -import kotlin.uuid.Uuid val postgresGenRandomUuid = CustomFunction("gen_random_uuid", UUIDColumnType()) 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 bfd49a97..1cfa9cef 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -8,6 +8,7 @@ import com.zenmo.ztor.deeplink.DeeplinkService import com.zenmo.ztor.errorMessageToJson import com.zenmo.ztor.user.getUserId import com.zenmo.zummon.companysurvey.Survey +import com.zenmo.zummon.companysurvey.Project import io.ktor.http.* import io.ktor.serialization.* import io.ktor.server.application.* @@ -35,6 +36,57 @@ fun Application.configureDatabases(): Database { call.respond(HttpStatusCode.OK, projectRepository.getProjectsByUserId(userId)) } + // Create + post("/projects") { + val project: Project? + try { + project = 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 + } + + val newProject = projectRepository.save(project) + + call.respond(HttpStatusCode.Created, newProject) + } + + get("/projects/{projectId}") { + val projectId = UUID.fromString(call.parameters["projectId"]) + + val userId = call.getUserId() + if (userId == null) { // Check if the user have access to the project + call.respond(HttpStatusCode.Unauthorized) + return@get + } + + call.respond(HttpStatusCode.OK, projectRepository.getProjectById(projectId)) + } + + // set active state + put("/projects/{projectId}") { + val projectId = UUID.fromString(call.parameters["projectId"]) + val project: Project? + try { + project = 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 + } + + val newProject = projectRepository.save(project) + + call.respond(HttpStatusCode.OK) + } + get("/projects/by-name/{projectName}/buurtcodes") { val projectName = call.parameters["projectName"]!! call.respond(HttpStatusCode.OK, projectRepository.getBuurtCodesByProjectName(projectName)) diff --git a/zummon/src/commonMain/kotlin/companysurvey/Project.kt b/zummon/src/commonMain/kotlin/companysurvey/Project.kt index 110fd045..edaf18db 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Project.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Project.kt @@ -1,9 +1,10 @@ package com.zenmo.zummon.companysurvey +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuid4 import com.zenmo.zummon.BenasherUuidSerializer import kotlinx.serialization.Serializable import kotlin.js.JsExport -import kotlin.uuid.Uuid @Serializable @JsExport @@ -11,7 +12,7 @@ data class Project constructor( // @Contextual @Serializable(with = BenasherUuidSerializer::class) - val id: Uuid, + val id: Uuid = uuid4(), val name: String = "", // Project ID aka Energy Hub ID of Energieke Regio. val energiekeRegioId: Int?, @@ -22,3 +23,8 @@ constructor( fun projectsFromJson(json: String): Array { return kotlinx.serialization.json.Json.decodeFromString>(json) } + +@JsExport +fun projectFromJson(json: String): Project { + return kotlinx.serialization.json.Json.decodeFromString(json) +} From b81fc1d8792819615b5c80a7b8d18151a2546e27 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 16 Dec 2024 16:36:41 +0000 Subject: [PATCH 4/9] View and edit The cancel is not working --- frontend/src/admin/edit-button.tsx | 2 +- frontend/src/admin/project-form.tsx | 60 +++++++++++++++++++---------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/frontend/src/admin/edit-button.tsx b/frontend/src/admin/edit-button.tsx index 49c3d7b2..818efa13 100644 --- a/frontend/src/admin/edit-button.tsx +++ b/frontend/src/admin/edit-button.tsx @@ -5,6 +5,6 @@ export const EditButton = ({type, id}: {type: string, id: string}) => ( textDecoration: 'none', whiteSpace: 'nowrap', }}> - + ) diff --git a/frontend/src/admin/project-form.tsx b/frontend/src/admin/project-form.tsx index f77436ec..78337c98 100644 --- a/frontend/src/admin/project-form.tsx +++ b/frontend/src/admin/project-form.tsx @@ -9,8 +9,24 @@ import { redirectToLogin } from "./use-projects"; export const ProjectForm: FunctionComponent = () => { const { projectId } = useParams<{ projectId: string }>(); const [project, setProject] = useState(null); + const [originalData, setOriginalData] = useState(null); const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const [isEditing, setIsEditing] = useState(false); + + const handleEditToggle = () => { + if (isEditing) { + setProject((prevProject) => originalData as Project); + } else { + setOriginalData((prevData) => project as Project); + } + setIsEditing(!isEditing); + }; + + const handleInputChange =(e: React.ChangeEvent) => { + const { name, value } = e.target; + setProject((prev) => ({ ...prev, [name]: value } as Project)); + }; useEffect(() => { if (projectId) { @@ -42,6 +58,8 @@ export const ProjectForm: FunctionComponent = () => { const handleSubmit = async (event: FormEvent) => { event.preventDefault(); + setOriginalData(project); // Update original data on successful submit + setIsEditing(false); setLoading(true); try { const method = projectId ? "PUT" : "POST"; @@ -75,41 +93,43 @@ export const ProjectForm: FunctionComponent = () => { } }; - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setProject((prev) => ({ ...prev, [name]: value } as Project)); - }; - - const groupStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - gap: "1rem", - maxWidth: "400px", - margin: "0 auto", - }; - return ( -
+

{projectId ? "Edit Project" : "Add Project"}

-
+ -
From 81d2d78343b9c94d42e6e3c4b62ed491f4bf6ad2 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 16 Dec 2024 20:00:20 +0000 Subject: [PATCH 5/9] Update project-form.tsx Save original data to cancel. still needs work --- frontend/src/admin/project-form.tsx | 45 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/frontend/src/admin/project-form.tsx b/frontend/src/admin/project-form.tsx index 78337c98..08054cc0 100644 --- a/frontend/src/admin/project-form.tsx +++ b/frontend/src/admin/project-form.tsx @@ -10,17 +10,21 @@ export const ProjectForm: FunctionComponent = () => { const { projectId } = useParams<{ projectId: string }>(); const [project, setProject] = useState(null); const [originalData, setOriginalData] = useState(null); + const [loading, setLoading] = useState(false); - const navigate = useNavigate(); const [isEditing, setIsEditing] = useState(false); + const navigate = useNavigate(); - const handleEditToggle = () => { - if (isEditing) { - setProject((prevProject) => originalData as Project); - } else { - setOriginalData((prevData) => project as Project); + const handleCancel = () => { + if (originalData) { + setProject(originalData); // Revert to original data } - setIsEditing(!isEditing); + setIsEditing(false); + }; + + // Handle toggle edit mode + const handleEditToggle = () => { + setIsEditing(true); }; const handleInputChange =(e: React.ChangeEvent) => { @@ -43,6 +47,7 @@ export const ProjectForm: FunctionComponent = () => { if (response.ok) { const projectData = await response.json(); setProject(projectData); + setOriginalData(projectData); } else { alert(`Error fetching project: ${response.statusText}`); } @@ -58,8 +63,6 @@ export const ProjectForm: FunctionComponent = () => { const handleSubmit = async (event: FormEvent) => { event.preventDefault(); - setOriginalData(project); // Update original data on successful submit - setIsEditing(false); setLoading(true); try { const method = projectId ? "PUT" : "POST"; @@ -81,6 +84,8 @@ export const ProjectForm: FunctionComponent = () => { if (response.ok) { const projectData = await response.json(); alert(`Project ${projectId ? "updated" : "created"} successfully!`); + setProject(projectData); + setOriginalData(projectData); navigate(`/projects/${projectData.id}`); } else { const errorData = await response.json(); @@ -89,6 +94,7 @@ export const ProjectForm: FunctionComponent = () => { } catch (error) { alert((error as Error).message); } finally { + setIsEditing(false); setLoading(false); } }; @@ -105,7 +111,7 @@ export const ProjectForm: FunctionComponent = () => { @@ -113,22 +119,21 @@ export const ProjectForm: FunctionComponent = () => {
-
From aba943f55068752065b72b2c48b3aaf4e28e3475 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Tue, 17 Dec 2024 15:38:03 +0000 Subject: [PATCH 6/9] Display only project from current user --- frontend/src/admin/project-form.tsx | 10 ++---- .../orm/companysurvey/ProjectRepository.kt | 12 +++++-- .../com/zenmo/ztor/plugins/Databases.kt | 31 ++++++++++--------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/frontend/src/admin/project-form.tsx b/frontend/src/admin/project-form.tsx index 08054cc0..d882126f 100644 --- a/frontend/src/admin/project-form.tsx +++ b/frontend/src/admin/project-form.tsx @@ -22,7 +22,6 @@ export const ProjectForm: FunctionComponent = () => { setIsEditing(false); }; - // Handle toggle edit mode const handleEditToggle = () => { setIsEditing(true); }; @@ -82,11 +81,8 @@ export const ProjectForm: FunctionComponent = () => { return; } if (response.ok) { - const projectData = await response.json(); alert(`Project ${projectId ? "updated" : "created"} successfully!`); - setProject(projectData); - setOriginalData(projectData); - navigate(`/projects/${projectData.id}`); + navigate(`/projects/${projectId}`); } else { const errorData = await response.json(); alert(`Error: ${errorData.message}`); @@ -111,6 +107,7 @@ export const ProjectForm: FunctionComponent = () => { { diff --git a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt index 6a6da6de..b430d286 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt @@ -8,8 +8,6 @@ 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.Uuid -import kotlin.uuid.toKotlinUuid class ProjectRepository( val db: Database @@ -32,6 +30,16 @@ class ProjectRepository( ).first() } + fun getProjectByUserId(userId: UUID, projectId: UUID): Project { + return getProjects( + (ProjectTable.id eq projectId) and + (ProjectTable.id eq anyFrom( + UserProjectTable.select(UserProjectTable.projectId) + .where { UserProjectTable.userId eq userId } + )) + ).first() + } + fun getProjectsByUserId(userId: UUID): List = transaction(db) { getProjects( 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 1cfa9cef..a0d87e0c 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -26,6 +26,7 @@ fun Application.configureDatabases(): Database { val deeplinkService = DeeplinkService(DeeplinkRepository(db)) routing { + // List projects for current user get("/projects") { val userId = call.getUserId() if (userId == null) { // Check if it's admin to return all the projects @@ -36,6 +37,19 @@ 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() + if (userId == null) { // Check if the user have access to the project + call.respond(HttpStatusCode.Unauthorized) + return@get + } + + call.respond(HttpStatusCode.OK, projectRepository.getProjectByUserId(userId, projectId)) + } + // Create post("/projects") { val project: Project? @@ -55,21 +69,8 @@ fun Application.configureDatabases(): Database { call.respond(HttpStatusCode.Created, newProject) } - get("/projects/{projectId}") { - val projectId = UUID.fromString(call.parameters["projectId"]) - - val userId = call.getUserId() - if (userId == null) { // Check if the user have access to the project - call.respond(HttpStatusCode.Unauthorized) - return@get - } - - call.respond(HttpStatusCode.OK, projectRepository.getProjectById(projectId)) - } - - // set active state + // Update put("/projects/{projectId}") { - val projectId = UUID.fromString(call.parameters["projectId"]) val project: Project? try { project = call.receive() @@ -84,7 +85,7 @@ fun Application.configureDatabases(): Database { val newProject = projectRepository.save(project) - call.respond(HttpStatusCode.OK) + call.respond(HttpStatusCode.OK, newProject) } get("/projects/by-name/{projectName}/buurtcodes") { From ef33294bad672fdce2c19e792227addeaec9ad2b Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Tue, 17 Dec 2024 17:24:28 +0000 Subject: [PATCH 7/9] Use form for create new project --- frontend/src/admin/project-form.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/admin/project-form.tsx b/frontend/src/admin/project-form.tsx index d882126f..5dc0bea5 100644 --- a/frontend/src/admin/project-form.tsx +++ b/frontend/src/admin/project-form.tsx @@ -7,7 +7,7 @@ import { Project } from "zero-zummon"; // Assuming this is the project model import { redirectToLogin } from "./use-projects"; export const ProjectForm: FunctionComponent = () => { - const { projectId } = useParams<{ projectId: string }>(); + const {projectId} = useParams<{ projectId: string }>(); const [project, setProject] = useState(null); const [originalData, setOriginalData] = useState(null); @@ -57,6 +57,8 @@ export const ProjectForm: FunctionComponent = () => { } }; fetchProject(); + } else { + setIsEditing(true); } }, [projectId]); @@ -108,7 +110,7 @@ export const ProjectForm: FunctionComponent = () => { id="name" name="name" value={project?.name} - defaultValue={project?.name} + defaultValue={project?.name || ""} onChange={handleInputChange} disabled={!isEditing} /> @@ -116,7 +118,7 @@ export const ProjectForm: FunctionComponent = () => { From 9f433af56f7c87f049a6f218630d0890e0618b03 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Fri, 3 Jan 2025 00:25:49 -0500 Subject: [PATCH 8/9] Assing Projects to the current user The user that creates the project should have access to the project by default. If it's Admin it won't be assigned - to do --- frontend/src/admin/project-form.tsx | 11 ++--- frontend/src/admin/use-projects.ts | 42 ------------------- .../orm/companysurvey/ProjectRepository.kt | 29 ++++++++++--- .../companysurvey/ProjectRepositoryTest.kt | 22 ++++++++++ .../com/zenmo/ztor/plugins/Databases.kt | 21 ++++++++-- 5 files changed, 67 insertions(+), 58 deletions(-) diff --git a/frontend/src/admin/project-form.tsx b/frontend/src/admin/project-form.tsx index 5dc0bea5..3326da75 100644 --- a/frontend/src/admin/project-form.tsx +++ b/frontend/src/admin/project-form.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { PrimeReactProvider } from "primereact/api"; import { InputText } from "primereact/inputtext"; import { Button } from "primereact/button"; -import { Project } from "zero-zummon"; // Assuming this is the project model +import { Project } from "zero-zummon"; import { redirectToLogin } from "./use-projects"; export const ProjectForm: FunctionComponent = () => { @@ -82,15 +82,13 @@ export const ProjectForm: FunctionComponent = () => { redirectToLogin(); return; } + if (response.ok) { - alert(`Project ${projectId ? "updated" : "created"} successfully!`); - navigate(`/projects/${projectId}`); + navigate(`/projects`); } else { const errorData = await response.json(); alert(`Error: ${errorData.message}`); } - } catch (error) { - alert((error as Error).message); } finally { setIsEditing(false); setLoading(false); @@ -109,8 +107,7 @@ export const ProjectForm: FunctionComponent = () => { diff --git a/frontend/src/admin/use-projects.ts b/frontend/src/admin/use-projects.ts index 17350a48..d45749c2 100644 --- a/frontend/src/admin/use-projects.ts +++ b/frontend/src/admin/use-projects.ts @@ -56,48 +56,6 @@ export const useProjects = (): UseProjectReturn => { } } -export const useCreateProject = (): UseProjectData => { - const [loadingProject, setLoading] = useState(true) - const [project, setProject] = useState() - const navigate = useNavigate(); - - useOnce(async () => { - try { - const response = await fetch(import.meta.env.VITE_ZTOR_URL + '/projects', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify(project), - }) - if (response.status === 401) { - redirectToLogin() - return - } - - if (response.ok) { - const createdProject = await response.json(); - alert(`Project created successfully with ID: ${createdProject.id}`); - setProject(createdProject) - navigate(`/projects/${createdProject.id}`); - } else { - const errorData = await response.json(); - alert(`Error creating project: ${errorData.message}`); - } - } catch (error) { - alert((error as Error).message) - } finally { - setLoading(false) - } - }) - - return { - loadingProject, - project, - } -} - export const redirectToLogin = () => { window.location.href = import.meta.env.VITE_ZTOR_URL + '/login?redirectUrl=' + encodeURIComponent(window.location.href) } diff --git a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt index b430d286..5f66d00f 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/ProjectRepository.kt @@ -2,6 +2,8 @@ package com.zenmo.orm.companysurvey import com.zenmo.zummon.companysurvey.Project import com.zenmo.orm.user.table.UserProjectTable +import com.zenmo.orm.user.table.UserTable + import com.zenmo.orm.companysurvey.table.ProjectTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq @@ -58,14 +60,29 @@ class ProjectRepository( @OptIn(ExperimentalUuidApi::class) fun save(project: Project): Project { + return saveProject(project) + } + + @OptIn(ExperimentalUuidApi::class) + fun saveToUser(project: Project, userId: UUID) { + transaction(db) { + val savedProject = saveProject(project) + UserProjectTable.insert { + it[projectId] = UUID.fromString(savedProject.id.toString()) + it[UserProjectTable.userId] = userId + } + } + } + + private fun saveProject(project: Project): Project { return transaction(db) { - ProjectTable.upsertReturning() { - it[id] = UUID.fromString(project.id.toString()) - it[name] = project.name - it[energiekeRegioId] = project.energiekeRegioId - it[buurtCodes] = project.buurtCodes + ProjectTable.upsertReturning() { + it[id] = UUID.fromString(project.id.toString()) + it[name] = project.name + it[energiekeRegioId] = project.energiekeRegioId + it[buurtCodes] = project.buurtCodes }.map { - hydrateProject(it) + hydrateProject(it) }.first() } } diff --git a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/ProjectRepositoryTest.kt b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/ProjectRepositoryTest.kt index 9c36f048..65f230c4 100644 --- a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/ProjectRepositoryTest.kt +++ b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/ProjectRepositoryTest.kt @@ -71,6 +71,28 @@ class ProjectRepositoryTest { } } + @Test + fun testSaveNewProjectToUser() { + val userId = UUID.randomUUID() + userRepository.saveUser(userId) + + val projectName = "User's Project 1" + + val project = Project( + name = projectName, + energiekeRegioId = 123, + ) + + val newProject = projectRepository.save(project) + projectRepository.saveToUser(newProject, userId) + + transaction(db) { + val projects = projectRepository.getProjectsByUserId(userId) + assertEquals(1, projects.size) + assertTrue(projects.any { it.name == projectName }) + } + } + @Test fun testGetProjectByEnergiekeRegioId() { val project = Project( 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 a0d87e0c..d5e38cfd 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -4,6 +4,7 @@ import com.zenmo.orm.companysurvey.ProjectRepository import com.zenmo.orm.companysurvey.SurveyRepository import com.zenmo.orm.connectToPostgres import com.zenmo.orm.deeplink.DeeplinkRepository +import com.zenmo.orm.user.UserRepository import com.zenmo.ztor.deeplink.DeeplinkService import com.zenmo.ztor.errorMessageToJson import com.zenmo.ztor.user.getUserId @@ -21,6 +22,7 @@ import java.util.* fun Application.configureDatabases(): Database { val db: Database = connectToPostgres() + val userRepository = UserRepository(db) val surveyRepository = SurveyRepository(db) val projectRepository = ProjectRepository(db) val deeplinkService = DeeplinkService(DeeplinkRepository(db)) @@ -28,8 +30,11 @@ fun Application.configureDatabases(): Database { routing { // List projects for current user get("/projects") { + val userId = call.getUserId() - if (userId == null) { // Check if it's admin to return all the projects + println("User ID: $userId") + + if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@get } @@ -42,7 +47,8 @@ fun Application.configureDatabases(): Database { val projectId = UUID.fromString(call.parameters["projectId"]) val userId = call.getUserId() - if (userId == null) { // Check if the user have access to the project + println("User ID: $userId") + if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@get } @@ -64,7 +70,14 @@ fun Application.configureDatabases(): Database { return@post } - val newProject = projectRepository.save(project) + val userId = call.getUserId() + println("User ID: $userId") + if (userId == null) { + call.respond(HttpStatusCode.Unauthorized) + return@post + } + + val newProject = projectRepository.saveToUser(project, userId) call.respond(HttpStatusCode.Created, newProject) } @@ -172,6 +185,7 @@ 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 @@ -209,6 +223,7 @@ 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 From 9be103dd1252c0e73de053ed30e37b974dfed3e1 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Fri, 3 Jan 2025 00:40:44 -0500 Subject: [PATCH 9/9] Clean code --- frontend/src/components/company-survey-v2/project.tsx | 9 --------- ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt | 5 +---- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/frontend/src/components/company-survey-v2/project.tsx b/frontend/src/components/company-survey-v2/project.tsx index d191fc37..50f9a6e9 100644 --- a/frontend/src/components/company-survey-v2/project.tsx +++ b/frontend/src/components/company-survey-v2/project.tsx @@ -48,12 +48,3 @@ export async function getProjectConfiguration(projectName: ProjectName): Promise buurtcodes, } } - -export async function getProjectById(projectId: string): Promise { - try { - return await ztorFetch(`/projects/${projectId}`); - } catch (error) { - console.error(`An error occurred while fetching the project by ID: ${projectId}`, error); - return null; - } -} 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 d5e38cfd..0a437bc8 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -30,10 +30,7 @@ fun Application.configureDatabases(): Database { routing { // List projects for current user get("/projects") { - val userId = call.getUserId() - println("User ID: $userId") - if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@get @@ -83,7 +80,7 @@ fun Application.configureDatabases(): Database { } // Update - put("/projects/{projectId}") { + put("/projects") { val project: Project? try { project = call.receive()