diff --git a/compose.yaml b/compose.yaml index 9be5ab2f..270b8d07 100644 --- a/compose.yaml +++ b/compose.yaml @@ -133,6 +133,7 @@ services: command: ztor:run ports: - 127.0.0.1:8082:8082 + - 127.0.0.1:5005:5005 environment: BASE_URL: http://localhost:8082 CORS_ALLOW_ORIGIN_PATTERN: 'http://localhost:\d{2,5}' diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 9053a53d..26fd35c9 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1,6 +1,6 @@ import {render, screen} from '@testing-library/react' import React from 'react' -import App from './App' +import App from './Simulation' test('renders learn react link', () => { render() diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a40e3a1..925b447c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,54 +1,13 @@ -import 'leaflet/dist/leaflet.css' -import React, {createElement as h, useState} from 'react' -import './App.css' -import {AggregatedAreaData} from './components/aggregated-area-data' -import {AnyLogic} from './components/any-logic' -import {BuurtPicker} from './components/buurt-picker' -import {MainMap} from './components/main-map' -import {PandDataDisplay} from './components/pand-display' -import {useApp} from './services/appState' -import {assertDefined} from './services/util' -import {Buurt} from './services/wijkenbuurten/buurten' -import {ZeroLayout} from "./components/zero-layout" - -function App() { - const appHook = useApp() - const {setGeometry, getPandData, bag2dPanden} = appHook - - const [currentPandId, setCurrentPandId] = useState('') - - const [buurt, setBuurt] = useState() +import React, {FunctionComponent} from 'react' +import {ZeroHeader} from "./components/zero-header"; +import {Outlet} from "react-router-dom"; +import {Dashboard} from "./components/dashboard"; +export const App: FunctionComponent = () => { return ( - - {/* Three-column layout*/} -
-
- {h(AggregatedAreaData, {appHook: appHook})} - { - setBuurt(buurt) - setGeometry(buurt.geometry) - }}/> -
-
- - -
-
- {currentPandId && getPandData(currentPandId) && - } -
-
-
- ) -} - -export default App + <> + + + + ); +}; diff --git a/frontend/src/Router/router.tsx b/frontend/src/Router/router.tsx new file mode 100644 index 00000000..99b8af13 --- /dev/null +++ b/frontend/src/Router/router.tsx @@ -0,0 +1,82 @@ +import { createBrowserRouter } from "react-router-dom"; + +import {Users} from "../admin/users"; +import {Projects} from "../admin/projects"; +import {Surveys} from "../admin/surveys"; + +import {ThankYou} from '../components/thank-you' +import {LoginWidget} from "../user/login"; +import {BedrijvenFormV1} from "../components/bedrijven-form-v1"; +import Simulation from "../Simulation"; + +import {Survey} from '../components/company-survey-v2/survey' +import {Intro} from "../components/intro"; +import {SurveyById, SurveyByIdRouteData} from "../components/company-survey-v2/survey-by-id"; +import {ExcelImport} from "../excel-import/excel-import" +import {NewSurveyByProjectName} from "../components/company-survey-v2/new-survey-by-project-name" +import {DE_WIEKEN, HESSENPOORT} from '../components/company-survey-v2/project' +import {Dashboard} from "../components/dashboard"; +import {App} from "../App"; + +export const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + {path: "", element: }, + {path: "/users", element: }, + {path: "/projects", element: }, + {path: "/surveys", element: }, + {path: "/simulation", element: }, + {path: "/intro", element: }, + ], + }, + { + path: "/bedrijven-v1", + element: + }, + { + path: "/new-survey/:projectName", + element: , + }, + { + path: "/bedrijven-hessenpoort", + element: , + }, + { + path: "/bedrijven-de-wieken", + element: , + }, + { + path: "/bedrijven-uitvraag/:surveyId", + element: , + loader: ({params: {surveyId}, request}): SurveyByIdRouteData => { + if (!surveyId) { + throw new Error("Survey ID is required") + } + const url = new URL(request.url); + const deeplink = url.searchParams.get("deeplink"); + const secret = url.searchParams.get("secret"); + + return { + surveyId, + deeplink, + secret, + } + } + }, + { + path: "/bedankt", + element: , + }, + { + path: "/admin/import-excel", + element: + }, + + { + path: "/login", + element: + } +]); + diff --git a/frontend/src/Simulation.tsx b/frontend/src/Simulation.tsx new file mode 100644 index 00000000..0457feb4 --- /dev/null +++ b/frontend/src/Simulation.tsx @@ -0,0 +1,55 @@ +import 'leaflet/dist/leaflet.css' +import React, {createElement as h, useState} from 'react' +import './App.css' +import {AggregatedAreaData} from './components/aggregated-area-data' +import {AnyLogic} from './components/any-logic' +import {BuurtPicker} from './components/buurt-picker' +import {MainMap} from './components/main-map' +import {PandDataDisplay} from './components/pand-display' +import {useApp} from './services/appState' +import {assertDefined} from './services/util' +import {Buurt} from './services/wijkenbuurten/buurten' +import {ZeroLayout} from "./components/zero-layout" + +function Simulation() { + const appHook = useApp() + const {setGeometry, getPandData, bag2dPanden} = appHook + + const [currentPandId, setCurrentPandId] = useState('') + + const [buurt, setBuurt] = useState() + + return ( + <> +

Simuleer je buurt

+ {/* Three-column layout*/} +
+
+ {h(AggregatedAreaData, {appHook: appHook})} + { + setBuurt(buurt) + setGeometry(buurt.geometry) + }}/> +
+
+ + +
+
+ {currentPandId && getPandData(currentPandId) && + } +
+
+ + ) +} + +export default Simulation diff --git a/frontend/src/admin/admin.tsx b/frontend/src/admin/admin.tsx deleted file mode 100644 index 06f65d4c..00000000 --- a/frontend/src/admin/admin.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import {FunctionComponent} from "react"; -import { DataTable } from 'primereact/datatable'; -import { Column } from 'primereact/column'; -import {useSurveys} from "./use-surveys"; -import {PrimeReactProvider} from "primereact/api"; -import {Survey, formatByteSize} 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 {JsonButton} from "./json-button"; -import {DeeplinkButton} from "./deeplink-button" -import {ZeroLayout} from "../components/zero-layout" - -import {AdminButtonRow} from "./admin-button-row" -import {SurveyIncludeInSimulationCheckbox} from "./survey-include-in-simulation-checkbox" - -export const Admin: FunctionComponent = () => { - const {loading, surveys, changeSurvey, removeSurvey} = useSurveys() - - const multipleProjects = surveys.map(survey => survey.zenmoProject) - .filter((value, index, self) => self.indexOf(value) === index).length > 1 - - return ( - - -
- -
- - {multipleProjects && } - - - - - {/* TODO: bestanden */} - ( - <> - {survey.filesArray.map(file => ( -
- {file.originalName} -   - ({formatByteSize(file.size)}) -
- ))} - - )}/> - - formatDatetime(survey.createdAt.toString())} header="Opgestuurd op" sortable/> - - changeSurvey(survey.withIncludeInSimulation(includeInSimulation))} - />}/> - ( -
*': { - margin: `${1/6}rem` - }, - }}> - - - - -
- )}/> -
-
-
- ) -} - -const downloadUrl = (blobName: string) => - import.meta.env.VITE_ZTOR_URL + "/download?blobName=" + encodeURIComponent(blobName) - -// Doing it in JavaScript because no timezone available in Kotlin. -const formatDatetime = (date: string) => { - const dateTime = new Date(date) - - return dateTime.getFullYear() + "-" + - (dateTime.getMonth() + 1).toString().padStart(2, "0") + "-" + - dateTime.getDate().toString().padStart(2, "0") + - " " + dateTime.getHours().toString().padStart(2, "0") + - ":" + dateTime.getMinutes().toString().padStart(2, "0") -} diff --git a/frontend/src/admin/projects.tsx b/frontend/src/admin/projects.tsx new file mode 100644 index 00000000..5f621386 --- /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` + }, + }}> + + +
+ )}/> +
+
+ ) +} diff --git a/frontend/src/admin/surveys.tsx b/frontend/src/admin/surveys.tsx new file mode 100644 index 00000000..34a47d89 --- /dev/null +++ b/frontend/src/admin/surveys.tsx @@ -0,0 +1,104 @@ +import React, {FunctionComponent} from "react"; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import {useSurveys} from "./use-surveys"; +import {PrimeReactProvider} from "primereact/api"; +import {Survey, formatByteSize} 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 {JsonButton} from "./json-button"; +import {DeeplinkButton} from "./deeplink-button" + +import {AdminButtonRow} from "./admin-button-row" +import {SurveyIncludeInSimulationCheckbox} from "./survey-include-in-simulation-checkbox" + +export const Surveys: FunctionComponent = () => { + const {loading, surveys, changeSurvey, removeSurvey} = useSurveys() + + const multipleProjects = surveys.map(survey => survey.zenmoProject) + .filter((value, index, self) => self.indexOf(value) === index).length > 1 + + return ( + +
+

Surveys List

+
+ +
+
+ + + {multipleProjects && } + + + + + {/* TODO: bestanden */} + ( + <> + {survey.filesArray.map(file => ( +
+ {file.originalName} +   + ({formatByteSize(file.size)}) +
+ ))} + + )}/> + + formatDatetime(survey.createdAt.toString())} + header="Opgestuurd op" sortable/> + + changeSurvey(survey.withIncludeInSimulation(includeInSimulation))} + />}/> + ( +
*': { + margin: `${1 / 6}rem` + }, + }}> + + + + +
+ )}/> +
+
+ ) +} + +const downloadUrl = (blobName: string) => + import.meta.env.VITE_ZTOR_URL + "/download?blobName=" + encodeURIComponent(blobName) + +// Doing it in JavaScript because no timezone available in Kotlin. +const formatDatetime = (date: string) => { + const dateTime = new Date(date) + + return dateTime.getFullYear() + "-" + + (dateTime.getMonth() + 1).toString().padStart(2, "0") + "-" + + dateTime.getDate().toString().padStart(2, "0") + + " " + dateTime.getHours().toString().padStart(2, "0") + + ":" + dateTime.getMinutes().toString().padStart(2, "0") +} diff --git a/frontend/src/admin/tmp b/frontend/src/admin/tmp new file mode 100644 index 00000000..ea5dd786 --- /dev/null +++ b/frontend/src/admin/tmp @@ -0,0 +1,99 @@ +import React, {FunctionComponent} from "react"; + +import "primereact/resources/themes/lara-light-cyan/theme.css" +import 'primeicons/primeicons.css' +import {ToggleButton} from "primereact/togglebutton" + +import {useState} from "react"; +import {Users} from "../admin/users"; +import {Projects} from "../admin/projects"; +import {Surveys} from "../admin/surveys"; +import {DataTable} from "primereact/datatable"; +import {Column} from "primereact/column"; +import {useUsers} from "../admin/use-users"; + +export const Dashboard: FunctionComponent = () => { + const {loading, users, changeUser, removeUser} = useUsers() + + const [fixUsers, setUsers] = useState([{id: 1, name: 'User 1'}, {id: 2, name: 'User 2'}]); + const [projects, setProjects] = useState([{id: 1, name: 'Project 1', userId: 1}, {id: 2, name: 'Project 2', userId: 2}]); + const [surveys, setSurveys] = useState([{id: 1, name: 'Survey 1', projectId: 1}, {id: 2, name: 'Survey 2', projectId: 2}]); + + const [selectedUserId, setSelectedUserId] = useState(null); + const [selectedProjectId, setSelectedProjectId] = useState(null); + + const [selectedUser, setSelectedUser] = useState(null); + + const handleUserSelect = (userId: number) => { + setSelectedUserId(userId); + // setSelectedUser(userId); + setSelectedProjectId(null); + }; + + const handleProjectSelect = (projectId: number) => { + setSelectedProjectId(projectId); + }; + + const [showUsers, setShowUsers] = useState(true); + const [showProjects, setShowProjects] = useState(true); + const [showSurveys, setShowSurveys] = useState(true); + + return ( + +
+
+ setShowUsers(!showUsers)} onLabel="Hide Users" + offLabel="Show Users" css={{padding: '1em'}}/> + setShowProjects(!showProjects)} onLabel="Hide Projects" + offLabel="Show Projects" css={{padding: '1em'}}/> + setShowSurveys(!showSurveys)} onLabel="Hide Surveys" + offLabel="Show Surveys" css={{padding: '1em'}}/> +
+
+ {showUsers && ( +
+

Users

+ + + handleUserSelect(e.value.id.toString())} dataKey="id" tableStyle={{ minWidth: '50rem' }}> + {/**/} + + +
    + {fixUsers.map(user => ( +
  • handleUserSelect(user.id)}> + {user.name} +
  • + ))} +
+
+ )} + {showProjects && selectedUserId && ( +
+

Projects

+ +
    + {projects.filter(project => project.userId === selectedUserId).map(project => ( +
  • handleProjectSelect(project.id)}> + {project.name} +
  • + ))} +
+
+ )} + {showSurveys && selectedProjectId && ( +
+

Surveys

+ +
    + {surveys.filter(survey => survey.projectId === selectedProjectId).map(survey => ( +
  • {survey.name}
  • + ))} +
+
+ )} +
+
+ ); +}; \ 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..1fc5c85e --- /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) +} diff --git a/frontend/src/admin/use-users.ts b/frontend/src/admin/use-users.ts new file mode 100644 index 00000000..e034259d --- /dev/null +++ b/frontend/src/admin/use-users.ts @@ -0,0 +1,52 @@ +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 + } + + 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/users.tsx b/frontend/src/admin/users.tsx new file mode 100644 index 00000000..fb36210f --- /dev/null +++ b/frontend/src/admin/users.tsx @@ -0,0 +1,57 @@ +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 {useNavigate} from "react-router-dom" +import {Button} from "primereact/button"; + +export const Users: FunctionComponent = () => { + const {loadingUsers, users, changeUser, removeUser} = useUsers() + const navigate = useNavigate(); + + return ( + +
+

Users List

+
+ + + ( +
*': { + margin: `${1 / 6}rem` + }, + }}> + + +
+ )}/> +
+
+ ) +} diff --git a/frontend/src/components/dashboard.tsx b/frontend/src/components/dashboard.tsx new file mode 100644 index 00000000..f3489220 --- /dev/null +++ b/frontend/src/components/dashboard.tsx @@ -0,0 +1,100 @@ +import React, {FunctionComponent} from "react"; + +import "primereact/resources/themes/lara-light-cyan/theme.css" +import 'primeicons/primeicons.css' +import {ToggleButton} from "primereact/togglebutton" + +import {useState} from "react"; +import {DataTable} from "primereact/datatable"; +import {Column} from "primereact/column"; +import {InputText} from "primereact/inputtext"; + +import {useUsers} from "../admin/use-users"; +import {useProjects} from "../admin/use-projects"; +import {useSurveys} from "../admin/use-surveys"; + +export const Dashboard: FunctionComponent = () => { + const {loadingUsers, users, changeUser, removeUser} = useUsers() + + const {loadingProjects, projects, changeProject, removeProject} = useProjects() + const {loading, surveys, changeSurvey, removeSurvey} = useSurveys() + + const [selectedUser, setSelectedUser] = useState(null); + const [selectedProject, setSelectedProject] = useState(null); + const [selectedSurvey, setSelectedSurvey] = useState(null); + + const handleUserSelect = (user: any ) => { + setSelectedUser(user.value); + setSelectedProject(null); + }; + + const handleProjectSelect = (project: any) => { + setSelectedProject(project.value); + }; + + const [showUsers, setShowUsers] = useState(true); + const [showProjects, setShowProjects] = useState(true); + const [showSurveys, setShowSurveys] = useState(true); + + const onRowEditComplete = (e: any) => { + let _users = [...users]; + let { newData, index } = e; + + _users[index] = newData; + + changeUser(_users[index]); + }; + + const textEditor = (options: any) => { + return options.editorCallback(e.target.value)} />; + }; + + const allowEdit = (rowData: any) => { + return rowData.note !== 'Note'; + }; + + return ( + +
+
+ setShowUsers(!showUsers)} onLabel="Hide Users" + offLabel="Show Users" css={{padding: '1em'}}/> + setShowProjects(!showProjects)} onLabel="Hide Projects" + offLabel="Show Projects" css={{padding: '1em'}}/> + setShowSurveys(!showSurveys)} onLabel="Hide Surveys" + offLabel="Show Surveys" css={{padding: '1em'}}/> +
+
+ {showUsers && ( +
+

Users

+ + handleUserSelect(e)} dataKey="id" tableStyle={{ minWidth: '20rem' }}> + textEditor(options)} style={{ width: '20%' }}> + + +
+ )} + {showProjects && selectedUser && ( +
+

Projects

+ handleProjectSelect(e)} dataKey="id" tableStyle={{ minWidth: '20rem' }}> + + +
+ )} + {showSurveys && selectedProject && ( +
+

Surveys

+ setSelectedSurvey(e.value.id.toString())} dataKey="id" tableStyle={{ minWidth: '20rem' }}> + + +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/home.tsx b/frontend/src/components/intro.tsx similarity index 85% rename from frontend/src/components/home.tsx rename to frontend/src/components/intro.tsx index e3248db8..d6cc7efd 100644 --- a/frontend/src/components/home.tsx +++ b/frontend/src/components/intro.tsx @@ -2,8 +2,8 @@ import {FunctionComponent} from "react" import {ZeroLayout} from "./zero-layout" import {Link} from "react-router-dom" -export const Home: FunctionComponent = () => ( - +export const Intro: FunctionComponent = () => { + return (

Welkom bij Zenmo Zero

Deze pagina is in aanbouw. Wat u kunt vinden op deze website:

@@ -11,7 +11,7 @@ export const Home: FunctionComponent = () => (
  • Uitvraag Hessenpoort invullen
  • Uitvraag De Wieken invullen
  • Uitvragen beheren
  • -
  • Oud proof of concept "simuleer je buurt"
  • +
  • Oud proof of concept "simuleer je buurt"
  • Links naar andere plekken:

    -
    -) \ No newline at end of file +
    ) +} \ No newline at end of file diff --git a/frontend/src/components/zero-header.tsx b/frontend/src/components/zero-header.tsx index 4d92f27f..8ea35769 100644 --- a/frontend/src/components/zero-header.tsx +++ b/frontend/src/components/zero-header.tsx @@ -1,16 +1,70 @@ -import React from "react"; +import React, {FunctionComponent, PropsWithChildren, useState} from "react" +import {Button} from "primereact/button"; +import {Sidebar} from "primereact/sidebar"; +import {css} from "@emotion/react"; +import {To, useNavigate} from "react-router-dom"; -export const ZeroHeader = () => ( -

    - -   - Zenmo Zero -

    -) \ No newline at end of file +const sidebarStyle = css({ + width: '250px', + backgroundColor: '#f5f5f5', + borderRight: '1px solid #ddd', +}); + +const buttonStyle = css({ + display: 'block', + marginBottom: '45px', + width: '100%', + textAlign: 'left', + padding: '0.5em 1em', + border: 'none', + borderBottom: '1px solid #ddd', + color: '#333', + background: '#f5f5f5', + transition: 'background-color 0.2s ease-in-out', + fontWeight: 'normal', + cursor: 'pointer', + '&:hover': { + backgroundColor: '#ebebeb', + color: '#007ad9', + }, +}); + +export const ZeroHeader: FunctionComponent = () => { + const [visible, setVisible] = useState(false); + const navigate = useNavigate(); + + const loadContent = (navidateTo: To) => { + setVisible(false); + navigate(navidateTo) + } + return ( +
    +
    +
    + setVisible(false)} css={sidebarStyle}> +
    + ); +}; \ No newline at end of file diff --git a/frontend/src/components/zero-layout.tsx b/frontend/src/components/zero-layout.tsx index 2a136c3e..b6590dac 100644 --- a/frontend/src/components/zero-layout.tsx +++ b/frontend/src/components/zero-layout.tsx @@ -15,9 +15,9 @@ export const ZeroLayout: FunctionComponent Zenmo logo + alt="Zenmo logo" + src="https://zenmo.com/wp-content/uploads/elementor/thumbs/zenmo-logo-website-light-grey-square-o1piz2j6llwl7n0xd84ywkivuyf22xei68ewzwrvmc.png" + style={{height: "1em", verticalAlign: "sub"}}/>   Zenmo Zero diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 4e5067bf..d04ed5f7 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,94 +1,20 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import { - createBrowserRouter, - RouterProvider, -} from "react-router-dom"; -import './index.css' -import {DE_WIEKEN, HESSENPOORT} from './components/company-survey-v2/project' -import {Survey} from './components/company-survey-v2/survey' -import {ThankYou} from './components/thank-you' import reportWebVitals from './reportWebVitals' -import App from "./App"; -import {LoginWidget} from "./user/login"; -import {BedrijvenFormV1} from "./components/bedrijven-form-v1"; -import {Admin} from "./admin/admin"; -import {SurveyById, SurveyByIdRouteData} from "./components/company-survey-v2/survey-by-id"; -import {Home} from "./components/home" -import {ExcelImport} from "./excel-import/excel-import" -import {NewSurveyByProjectName} from "./components/company-survey-v2/new-survey-by-project-name" - -const router = createBrowserRouter([ - { - path: "/", - element: , - }, - { - path: "/proof-of-concept", - element: , - }, - { - path: "/bedrijven-v1", - element: - }, - { - path: "/new-survey/:projectName", - element: , - }, - { - path: "/bedrijven-hessenpoort", - element: , - }, - { - path: "/bedrijven-de-wieken", - element: , - }, - { - path: "/bedrijven-uitvraag/:surveyId", - element: , - loader: ({params: {surveyId}, request}): SurveyByIdRouteData => { - if (!surveyId) { - throw new Error("Survey ID is required") - } - const url = new URL(request.url); - const deeplink = url.searchParams.get("deeplink"); - const secret = url.searchParams.get("secret"); +import { RouterProvider } from "react-router-dom"; +import './index.css' - return { - surveyId, - deeplink, - secret, - } - } - }, - { - path: "/bedankt", - element: , - }, - { - path: "/admin", - element: , - }, - { - path: "/admin/import-excel", - element: - }, - { - path: "/login", - element: - } -]); +import { router } from "./Router/router"; const root = ReactDOM.createRoot( - //@ts-ignore - document.getElementById('react-root'), + document.getElementById('react-root') as HTMLElement, ) + root.render( , ) - // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 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 8ad7b902..773f44b5 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt @@ -3,6 +3,7 @@ package com.zenmo.orm.companysurvey import com.zenmo.orm.blob.BlobPurpose import com.zenmo.orm.companysurvey.table.* import com.zenmo.orm.companysurvey.table.GridConnectionTable.addressId +import com.zenmo.orm.user.UserRepository import com.zenmo.orm.user.table.UserProjectTable import com.zenmo.orm.user.table.UserTable import com.zenmo.zummon.companysurvey.* @@ -10,8 +11,6 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction import java.util.* -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.toKotlinUuid class SurveyRepository( private val db: Database, @@ -261,14 +260,10 @@ class SurveyRepository( ) } - @OptIn(ExperimentalUuidApi::class) protected fun hydrateUser(row: ResultRow): com.zenmo.zummon.User? { - val userId = row[CompanySurveyTable.createdById] ?: return null - - return com.zenmo.zummon.User( - row[UserTable.id].toKotlinUuid(), - row[UserTable.note], - ) + row[CompanySurveyTable.createdById] ?: return null + val userRepo = UserRepository(db) + return userRepo.hydrateUser(row) } protected fun hydrateAddress(row: ResultRow): Address { 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..e06055ea 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt @@ -1,10 +1,11 @@ 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 import com.zenmo.orm.companysurvey.table.ProjectTable -import com.zenmo.zummon.companysurvey.Project + import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction @@ -16,13 +17,14 @@ class UserRepository( ) { fun getUsers(filter: Op = Op.TRUE): List { return transaction(db) { - UserTable + val result = UserTable .selectAll() .where{ filter - }.mapNotNull { + }.map { hydrateUser(it) } + result } } @@ -88,7 +90,7 @@ class UserRepository( } } - protected fun hydrateUser(row: ResultRow): User { + fun hydrateUser(row: ResultRow): User { return User( id = row[UserTable.id], note = row[UserTable.note], diff --git a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/RepositoryTest.kt b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/RepositoryTest.kt index 72579634..ac6bf365 100644 --- a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/RepositoryTest.kt +++ b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/RepositoryTest.kt @@ -15,7 +15,6 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.toKotlinUuid class RepositoryTest { companion object { @@ -151,7 +150,7 @@ class RepositoryTest { val createdBy = surveyLoadedAfterCreate?.createdBy assertNotNull(createdBy) assertEquals("Jaap", createdBy.note) - assertEquals(jaapId.toKotlinUuid(), createdBy.id) + assertEquals(jaapId, createdBy.id) // edit survey repo.save(surveyLoadedAfterCreate, pietId) @@ -159,7 +158,7 @@ class RepositoryTest { val createdBy2 = surveyLoadedAfterEdit?.createdBy assertNotNull(createdBy2) assertEquals("Jaap", createdBy2.note) - assertEquals(jaapId.toKotlinUuid(), createdBy2.id) + assertEquals(jaapId, createdBy2.id) } private fun wipeSequence(survey: Survey) 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 6efae21f..4c59a721 100644 --- a/zorm/src/test/kotlin/com/zenmo/orm/user/UserRepositoryTest.kt +++ b/zorm/src/test/kotlin/com/zenmo/orm/user/UserRepositoryTest.kt @@ -13,7 +13,6 @@ import org.junit.BeforeClass import java.util.UUID import kotlin.test.* import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.toKotlinUuid class UserRepositoryTest { companion object { @@ -140,8 +139,8 @@ class UserRepositoryTest { user.projects.forEach { project -> when (project.id) { - project1Id.toKotlinUuid() -> assertEquals("Project 1", project.name) - project2Id.toKotlinUuid() -> assertEquals("Project 2", project.name) + project1Id -> assertEquals("Project 1", project.name) + project2Id -> assertEquals("Project 2", project.name) else -> fail("Unexpected project ID ${project.id}") } } diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt index a9260863..70cc8a62 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt @@ -90,7 +90,7 @@ fun Application.configureAuthentication() { } } } - call.respondRedirect("/home") + call.respondRedirect("/") } } get("/user-info") { @@ -102,7 +102,7 @@ fun Application.configureAuthentication() { call.respond(userSession.getDecodedAccessToken()) } } - get("/home") { + get("/") { val userSession: UserSession? = call.sessions.get() if (userSession != null) { val token = decodeAccessToken(userSession.token) 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 01301ba8..cfd3f4b9 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -2,6 +2,8 @@ package com.zenmo.ztor.plugins import com.zenmo.orm.companysurvey.ProjectRepository import com.zenmo.orm.companysurvey.SurveyRepository +import com.zenmo.orm.user.UserRepository + import com.zenmo.orm.connectToPostgres import com.zenmo.orm.deeplink.DeeplinkRepository import com.zenmo.ztor.deeplink.DeeplinkService @@ -24,6 +26,15 @@ fun Application.configureDatabases(): Database { val deeplinkService = DeeplinkService(DeeplinkRepository(db)) routing { + get("/users") { + val userId = call.getUserId() + if (userId == null) { //Check if it's admin to return all the Users + call.respond(HttpStatusCode.Unauthorized) + return@get + } + call.respond(HttpStatusCode.OK, UserRepository(db).getUsers()) + } + get("/projects") { val userId = call.getUserId() if (userId == null) { diff --git a/zummon/src/commonMain/kotlin/User.kt b/zummon/src/commonMain/kotlin/User.kt index d024f557..69504dc9 100644 --- a/zummon/src/commonMain/kotlin/User.kt +++ b/zummon/src/commonMain/kotlin/User.kt @@ -1,9 +1,10 @@ package com.zenmo.zummon +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuid4 import com.zenmo.zummon.companysurvey.Project import kotlinx.serialization.Serializable import kotlin.js.JsExport -import kotlin.uuid.Uuid /** * This object is intended to be enriched with Keycloak data. @@ -12,8 +13,13 @@ import kotlin.uuid.Uuid @Serializable @JsExport data class User( - @Serializable(with = KotlinUuidSerializer::class) - val id: Uuid = Uuid.random(), + @Serializable(with = BenasherUuidSerializer::class) + val id: Uuid = uuid4(), val note: String, val projects: List = emptyList() ) + +@JsExport +fun usersFromJson(json: String): Array { + return kotlinx.serialization.json.Json.decodeFromString>(json) +} diff --git a/zummon/src/commonMain/kotlin/companysurvey/Project.kt b/zummon/src/commonMain/kotlin/companysurvey/Project.kt index cfe21978..d99f1678 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Project.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Project.kt @@ -2,7 +2,7 @@ package com.zenmo.zummon.companysurvey import com.benasher44.uuid.Uuid import com.benasher44.uuid.uuid4 -import com.zenmo.zummon.KotlinUuidSerializer +import com.zenmo.zummon.BenasherUuidSerializer import kotlinx.serialization.Serializable import kotlin.js.JsExport @@ -11,10 +11,15 @@ import kotlin.js.JsExport data class Project constructor( // @Contextual - @Serializable(with = KotlinUuidSerializer::class) + @Serializable(with = BenasherUuidSerializer::class) val id: Uuid = uuid4(), 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) +} \ No newline at end of file