Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into production
Browse files Browse the repository at this point in the history
  • Loading branch information
Erikvv committed Jan 22, 2025
2 parents c3ae0b4 + 754eb4f commit 9645d32
Show file tree
Hide file tree
Showing 22 changed files with 534 additions and 38 deletions.
3 changes: 1 addition & 2 deletions frontend/src/admin/project-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/admin/use-users.ts
Original file line number Diff line number Diff line change
@@ -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<User[]>([])

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)
}
148 changes: 148 additions & 0 deletions frontend/src/admin/user-form.tsx
Original file line number Diff line number Diff line change
@@ -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<User | null>(null);
const [originalData, setOriginalData] = useState<User | null>(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<HTMLInputElement>) => {
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 (
<PrimeReactProvider>
<div style={{ padding: "20px", maxWidth: "500px", margin: "0 auto" }}>
<h3>{userId ? "Edit User" : "Add User"}</h3>
<form
onSubmit={handleSubmit}
style={{ display: "flex", flexDirection: "column", gap: "10px" }}
>
<label htmlFor="name">Keycloak ID:</label>
<InputText
id="id"
name="id"
value={user?.id || ""}
onChange={handleInputChange}
disabled={!isEditing}
/>
<label htmlFor="name">Note:</label>
<InputText
id="note"
name="note"
value={user?.note || ""}
onChange={handleInputChange}
disabled={!isEditing}
/>
<div>
<label htmlFor="isAdmin" style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input
type="checkbox"
id="isAdmin"
name="isAdmin"
checked={user?.isAdmin || false}
onChange={handleInputChange}
disabled={!isEditing}
/>
Admin
</label>
</div>

<div style={{ display: "flex", justifyContent: "space-between", marginTop: "10px" }}>
{isEditing ? (
<>
<Button label="Cancel" onClick={handleCancel} type="button" disabled={loading} />
<Button label={loading ? "Saving..." : "Save"} type="submit" disabled={loading} />
</>
) : (
<Button label="Edit" onClick={handleEditToggle} type="button" disabled={loading} />
)}
</div>
</form>
</div>
</PrimeReactProvider>
);
};
70 changes: 70 additions & 0 deletions frontend/src/admin/users.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PrimeReactProvider>
<div css={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1em 1em',
boxShadow: '1px solid #ddd'
}}>
<h3>Users List</h3>
<Button
label="Nieuw"
icon="pi pi-pencil"
onClick={(event) => navigate(`/users/new-user`)}
/>
</div>
<DataTable
value={users}
loading={loadingUsers}
sortField="created"
sortOrder={-1}
filterDisplay="row"
>
<Column field="note" header="Note" sortable filter/>
<Column
field="isAdmin"
header="Admin"
body={(user: User) => (
<div style={{ textAlign: 'center' }}>
{user.isAdmin ? (
<span style={{ color: 'green' }}></span>
) : (
<span style={{ color: 'red' }}></span>
)}
</div>
)}
/>
<Column body={(user: User) => (
<div css={{
display: 'flex',
'> *': {
margin: `${1 / 6}rem`
},
}}>
<DeleteButton type="users" id={user.id} onDelete={removeUser}/>
<EditButton type="users" id={user.id}/>
</div>
)}/>
</DataTable>
</PrimeReactProvider>
)
}
9 changes: 6 additions & 3 deletions frontend/src/components/company-survey-v2/survey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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',
Expand All @@ -144,6 +145,7 @@ const SurveyWithReset: FunctionComponent<{
return
}

localStorage.removeItem(localStorageKey)
navigate('/bedankt', {
state: {
deeplink: await response.json()
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Dropdown options={options} value={dateTimeUnitToIsoString(timeStep)} onChange={event => setTimeStep(isoStringToDateTimeUnit(event.value))}/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -50,6 +51,9 @@ export const TimeSeriesTextarea: FunctionComponent<{timeSeries: TimeSeries, setT
<LabelRow label="Tijdzone">
<Dropdown options={[{ label: 'Nederlandse tijd', value: displayTimeZone }]} value={displayTimeZone}/>
</LabelRow>
<LabelRow label="Meetinterval">
<IntervalDropdown timeStep={internalTimeSeries.timeStep} setTimeStep={timeStep => setTimeSeriesImpl(internalTimeSeries.withTimeStep(timeStep))} />
</LabelRow>
<LabelRow label="Plak hier de waarden in kWh">
<InputTextarea
id="values"
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/zero-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export const ZeroHeader: FunctionComponent<PropsWithChildren & {}> = () => {
<i className="pi pi-fw pi-file" style={{marginRight: '0.5em'}}></i>
Projects
</a>
{/* <a onClick={() => loadContent('/users')} css={buttonStyle}>
<i className="pi pi-fw pi-file" style={{marginRight: '0.5em'}}></i>
Users
</a> */}
<a onClick={() => loadContent('/simulation')} css={buttonStyle}>
<i className="pi pi-fw pi-file" style={{marginRight: '0.5em'}}></i>
Simulation
Expand Down
Loading

0 comments on commit 9645d32

Please sign in to comment.