[test-entity]: front pt.2

This commit is contained in:
it-is-not-alright 2024-11-20 03:08:12 +04:00
parent a60304ca0f
commit 658a351d28
55 changed files with 498 additions and 240 deletions

View File

@ -3,4 +3,5 @@ export const WIND_ENDPOINTS = {
turbineType: 'api/wind/turbine_type', turbineType: 'api/wind/turbine_type',
parks: 'api/wind/parks', parks: 'api/wind/parks',
park: 'api/wind/park', park: 'api/wind/park',
parkTurbine: 'api/wind/park_turbine',
}; };

View File

@ -1,10 +1,11 @@
import { ApiResponse } from '@api/types'; import { ApiResponse } from '@api/types';
import { ParkFormValues } from '@components/pages/park-page/types';
import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types'; import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types';
import { api } from '../api'; import { api } from '../api';
import { WIND_ENDPOINTS } from './constants'; import { WIND_ENDPOINTS } from './constants';
import { Park, ParkTurbine, ParkWithTurbines, TurbineType } from './types'; import { Park, ParkTurbine, ParkWithTurbines, TurbineType } from './types';
import { packTurbineTypes } from './utils'; import { packPark, packParkTurbine, packTurbineTypes } from './utils';
export const getTurbineTypes = () => { export const getTurbineTypes = () => {
return api.get<TurbineType[]>(WIND_ENDPOINTS.turbines); return api.get<TurbineType[]>(WIND_ENDPOINTS.turbines);
@ -63,3 +64,47 @@ export const getParkWithTurbines = async (
error: parkPesponse.error || turbinesResponse.error || null, error: parkPesponse.error || turbinesResponse.error || null,
}; };
}; };
export const createPark = async (formValues: Partial<ParkFormValues>) => {
const parkPesponse = await api.post<Park>(
WIND_ENDPOINTS.park,
packPark(formValues),
);
await Promise.all(
formValues.turbines?.map((t) => {
return api.post(
WIND_ENDPOINTS.parkTurbine,
packParkTurbine(t, parkPesponse.data.id),
);
}),
);
return getParkWithTurbines(String(parkPesponse.data.id));
};
export const updatePark = async (
formValues: Partial<ParkFormValues>,
id: string,
) => {
const parkPesponse = await api.put<Park>(
`${WIND_ENDPOINTS.park}/${id}`,
packPark(formValues),
);
await Promise.all(
formValues.turbines?.map((t) => {
if (t.new) {
return api.post(
WIND_ENDPOINTS.parkTurbine,
packParkTurbine(t, parkPesponse.data.id),
);
}
if (t.delete) {
return api.delete(`${WIND_ENDPOINTS.parkTurbine}/${t.id}`);
}
return api.put(
`${WIND_ENDPOINTS.parkTurbine}/${parkPesponse.data.id}/${t.id}`,
packParkTurbine(t, parkPesponse.data.id),
);
}),
);
return getParkWithTurbines(id);
};

View File

@ -1,9 +1,32 @@
import {
ParkFormTurbine,
ParkFormValues,
} from '@components/pages/park-page/types';
import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types'; import { TurbineTypeFormValues } from '@components/pages/turbine-type-page/types';
export const packTurbineTypes = (values: Partial<TurbineTypeFormValues>) => { export const packTurbineTypes = (values: Partial<TurbineTypeFormValues>) => {
return { return {
Name: values.name ?? '', Name: values.name ?? '',
Height: parseInt(values.height ?? '0'), Height: parseInt(values.height || '0'),
BladeLength: parseInt(values.bladeLength ?? '0'), BladeLength: parseInt(values.bladeLength || '0'),
};
};
export const packPark = (values: Partial<ParkFormValues>) => {
return {
Name: values.name ?? '',
CenterLatitude: parseInt(values.centerLatitude || '0'),
CenterLongitude: parseInt(values.centerLongitude || '0'),
};
};
export const packParkTurbine = (turbine: ParkFormTurbine, parkId: number) => {
return {
wind_park_id: parkId,
turbine_id: turbine.id,
x_offset: parseInt(turbine.xOffset || '0'),
y_offset: parseInt(turbine.yOffset || '0'),
angle: parseInt(turbine.angle || '0'),
comment: turbine.comment ?? '',
}; };
}; };

View File

@ -1,11 +1,16 @@
import { getParkWithTurbines, ParkWithTurbines } from '@api/wind'; import {
import { Button, Heading, NumberInput, TextInput } from '@components/ui'; createPark,
getParkWithTurbines,
ParkWithTurbines,
updatePark,
} from '@api/wind';
import { Button, Heading, NumberField, TextInput } from '@components/ui';
import { ParkTurbines } from '@components/ux';
import { Controller, useForm } from '@utils/form'; import { Controller, useForm } from '@utils/form';
import { useRoute } from '@utils/route'; import { ROUTES, useRoute } from '@utils/route';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { ParkTurbines } from './components';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { ParkFormValues } from './types'; import { ParkFormValues } from './types';
import { unpackPark } from './utils'; import { unpackPark } from './utils';
@ -14,6 +19,7 @@ export function ParkPage() {
const [park, setPark] = useState<ParkWithTurbines>(null); const [park, setPark] = useState<ParkWithTurbines>(null);
const [pending, setPending] = useState<boolean>(false); const [pending, setPending] = useState<boolean>(false);
const params = useParams(); const params = useParams();
const navigate = useNavigate();
const route = useRoute(); const route = useRoute();
const { register, control, getValues, reset } = useForm<ParkFormValues>({}); const { register, control, getValues, reset } = useForm<ParkFormValues>({});
@ -35,6 +41,20 @@ export function ParkPage() {
fetchPark(); fetchPark();
}, [id]); }, [id]);
const handleFormSubmit = async (event: React.FormEvent) => {
event.preventDefault();
setPending(true);
if (isEdit) {
const response = await updatePark(getValues(), id);
setPark(response.data);
reset(unpackPark(response.data));
} else {
const response = await createPark(getValues());
navigate(ROUTES.park.path.replace(':id', String(response.data.id)));
}
setPending(false);
};
const handleReset = () => { const handleReset = () => {
if (isEdit) { if (isEdit) {
reset(unpackPark(park)); reset(unpackPark(park));
@ -44,7 +64,7 @@ export function ParkPage() {
}; };
return ( return (
<div className={styles.page}> <div className={styles.page} onSubmit={handleFormSubmit}>
<Heading tag="h1">{route.title}</Heading> <Heading tag="h1">{route.title}</Heading>
<form className={styles.form}> <form className={styles.form}>
<header> <header>
@ -55,13 +75,13 @@ export function ParkPage() {
<Controller <Controller
{...control('centerLatitude')} {...control('centerLatitude')}
render={(props) => ( render={(props) => (
<NumberInput label={{ text: 'Center Latitude' }} {...props} /> <NumberField label={{ text: 'Center Latitude' }} {...props} />
)} )}
/> />
<Controller <Controller
{...control('centerLongitude')} {...control('centerLongitude')}
render={(props) => ( render={(props) => (
<NumberInput label={{ text: 'Center Longitude' }} {...props} /> <NumberField label={{ text: 'Center Longitude' }} {...props} />
)} )}
/> />
</div> </div>
@ -76,7 +96,9 @@ export function ParkPage() {
</form> </form>
<Controller <Controller
{...control('turbines')} {...control('turbines')}
render={(props) => <ParkTurbines {...props} />} render={(props) => (
<ParkTurbines savedTurbines={park?.turbines ?? []} {...props} />
)}
/> />
</div> </div>
); );

View File

@ -1 +0,0 @@
export * from './park-turbines';

View File

@ -1,19 +0,0 @@
import { DataGrid } from '@components/ui/data-grid';
import React from 'react';
import { columns } from './constants';
import { ParkTurbinesProps } from './types';
export function ParkTurbines({ value = [] }: ParkTurbinesProps) {
return (
<div>
<div></div>
<DataGrid
items={value}
columns={columns}
getItemKey={({ id }) => String(id)}
selectedItems={[]}
/>
</div>
);
}

View File

@ -1,11 +0,0 @@
import { DataGridColumnConfig } from '@components/ui/data-grid/types';
import { ParkTurbine } from 'src/api/wind';
export const columns: DataGridColumnConfig<ParkTurbine>[] = [
{ name: 'Id', getText: (t) => String(t.id) },
{ name: 'Name', getText: (t) => t.name },
{ name: 'X', getText: (t) => String(t.xOffset) },
{ name: 'Y', getText: (t) => String(t.yOffset) },
{ name: 'Angle', getText: (t) => String(t.angle) },
{ name: 'Comment', getText: (t) => String(t.comment), flex: '2' },
];

View File

@ -1,6 +0,0 @@
import { ParkTurbine } from '@api/wind';
export type ParkTurbinesProps = {
value?: ParkTurbine[];
onChange?: (value: ParkTurbine[]) => void;
};

View File

@ -1,8 +1,17 @@
import { ParkTurbine } from '@api/wind'; export type ParkFormTurbine = {
id: number;
name: string;
xOffset: string;
yOffset: string;
angle: string;
comment: string;
new?: boolean;
delete?: boolean;
};
export type ParkFormValues = { export type ParkFormValues = {
name: string; name: string;
centerLatitude: string; centerLatitude: string;
centerLongitude: string; centerLongitude: string;
turbines: ParkTurbine[]; turbines: ParkFormTurbine[];
}; };

View File

@ -7,6 +7,13 @@ export const unpackPark = (park: ParkWithTurbines): ParkFormValues => {
name: park.name, name: park.name,
centerLatitude: String(park.centerLatitude), centerLatitude: String(park.centerLatitude),
centerLongitude: String(park.centerLongitude), centerLongitude: String(park.centerLongitude),
turbines: park.turbines, turbines: park.turbines.map((t) => ({
id: t.id,
name: t.name,
xOffset: String(t.xOffset),
yOffset: String(t.yOffset),
angle: String(t.angle),
comment: t.comment,
})),
}; };
}; };

View File

@ -2,7 +2,7 @@ import { DataGridColumnConfig } from '@components/ui/data-grid/types';
import { Park } from 'src/api/wind'; import { Park } from 'src/api/wind';
export const columns: DataGridColumnConfig<Park>[] = [ export const columns: DataGridColumnConfig<Park>[] = [
{ name: 'Name', getText: (t) => t.name, flex: '2' }, { name: 'Name', getText: (t) => t.name, width: 2 },
{ name: 'Center Latitude', getText: (t) => String(t.centerLatitude) }, { name: 'Center Latitude', getText: (t) => String(t.centerLatitude) },
{ name: 'Center Longitude', getText: (t) => String(t.centerLongitude) }, { name: 'Center Longitude', getText: (t) => String(t.centerLongitude) },
]; ];

View File

@ -12,7 +12,6 @@
.actions { .actions {
display: flex; display: flex;
padding: 10px; padding: 10px;
border: 1px solid var(--clr-border-100);
border-radius: 15px; border-radius: 15px;
background-color: var(--clr-layer-200); background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100); box-shadow: 0px 1px 2px var(--clr-shadow-100);

View File

@ -4,7 +4,7 @@ import {
getTurbineType, getTurbineType,
TurbineType, TurbineType,
} from '@api/wind'; } from '@api/wind';
import { Button, Heading, NumberInput, TextInput } from '@components/ui'; import { Button, Heading, NumberField, TextInput } from '@components/ui';
import { Controller, useForm } from '@utils/form'; import { Controller, useForm } from '@utils/form';
import { ROUTES, useRoute } from '@utils/route'; import { ROUTES, useRoute } from '@utils/route';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@ -47,6 +47,7 @@ export function TurbineTypePage() {
if (isEdit) { if (isEdit) {
const response = await editTurbineTypes(getValues(), id); const response = await editTurbineTypes(getValues(), id);
setTurbineType(response.data); setTurbineType(response.data);
reset(unpackTurbineType(response.data));
} else { } else {
const response = await createTurbineTypes(getValues()); const response = await createTurbineTypes(getValues());
navigate( navigate(
@ -76,13 +77,13 @@ export function TurbineTypePage() {
<Controller <Controller
{...control('height')} {...control('height')}
render={(props) => ( render={(props) => (
<NumberInput label={{ text: 'Height' }} {...props} /> <NumberField label={{ text: 'Height' }} {...props} />
)} )}
/> />
<Controller <Controller
{...control('bladeLength')} {...control('bladeLength')}
render={(props) => ( render={(props) => (
<NumberInput label={{ text: 'Blade length' }} {...props} /> <NumberField label={{ text: 'Blade length' }} {...props} />
)} )}
/> />
</div> </div>

View File

@ -2,7 +2,7 @@ import { DataGridColumnConfig } from '@components/ui/data-grid/types';
import { TurbineType } from 'src/api/wind'; import { TurbineType } from 'src/api/wind';
export const columns: DataGridColumnConfig<TurbineType>[] = [ export const columns: DataGridColumnConfig<TurbineType>[] = [
{ name: 'Name', getText: (t) => t.name, flex: '2' }, { name: 'Name', getText: (t) => t.name, width: 2 },
{ name: 'Height', getText: (t) => String(t.height) }, { name: 'Height', getText: (t) => String(t.height) },
{ name: 'Blade length', getText: (t) => String(t.bladeLength) }, { name: 'Blade length', getText: (t) => String(t.bladeLength) },
]; ];

View File

@ -12,7 +12,6 @@
.actions { .actions {
display: flex; display: flex;
padding: 10px; padding: 10px;
border: 1px solid var(--clr-border-100);
border-radius: 15px; border-radius: 15px;
background-color: var(--clr-layer-200); background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100); box-shadow: 0px 1px 2px var(--clr-shadow-100);

View File

@ -15,6 +15,11 @@ export function DataGrid<T>({
}: DataGridProps<T>) { }: DataGridProps<T>) {
const [allItemsSelected, setAllItemsSelected] = useState<boolean>(false); const [allItemsSelected, setAllItemsSelected] = useState<boolean>(false);
const columnsTemplate = useMemo(() => {
const main = columns.map((c) => `${c.width ?? 1}fr`).join(' ');
return `auto ${main}`;
}, []);
const selectedItemsMap = useMemo(() => { const selectedItemsMap = useMemo(() => {
const map: Record<string, T> = {}; const map: Record<string, T> = {};
for (let i = 0; i < selectedItems.length; i += 1) { for (let i = 0; i < selectedItems.length; i += 1) {
@ -35,7 +40,7 @@ export function DataGrid<T>({
const handleItemSelect = (key: string, item: T) => { const handleItemSelect = (key: string, item: T) => {
const selected = Boolean(selectedItemsMap[key]); const selected = Boolean(selectedItemsMap[key]);
if (!multiselect) { if (!multiselect) {
onItemsSelect(selected ? [] : [item]); onItemsSelect?.(selected ? [] : [item]);
return; return;
} }
onItemsSelect?.( onItemsSelect?.(
@ -52,6 +57,7 @@ export function DataGrid<T>({
columns={columns} columns={columns}
allItemsSelected={allItemsSelected} allItemsSelected={allItemsSelected}
onSelectAllItems={handleSelectAllItems} onSelectAllItems={handleSelectAllItems}
columnsTemplate={columnsTemplate}
/> />
{items.map((item) => { {items.map((item) => {
const key = String(getItemKey(item)); const key = String(getItemKey(item));
@ -62,6 +68,7 @@ export function DataGrid<T>({
selected={Boolean(selectedItemsMap[key])} selected={Boolean(selectedItemsMap[key])}
onSelect={() => handleItemSelect(key, item)} onSelect={() => handleItemSelect(key, item)}
key={getItemKey(item)} key={getItemKey(item)}
columnsTemplate={columnsTemplate}
/> />
); );
})} })}

View File

@ -14,6 +14,7 @@ export function DataGridHeader<T>({
columns, columns,
allItemsSelected, allItemsSelected,
onSelectAllItems, onSelectAllItems,
columnsTemplate,
}: DataGridHeaderProps<T>) { }: DataGridHeaderProps<T>) {
const [sort, setSort] = useState<DataGridSort>({ order: 'asc', column: '' }); const [sort, setSort] = useState<DataGridSort>({ order: 'asc', column: '' });
@ -30,7 +31,10 @@ export function DataGridHeader<T>({
}; };
return ( return (
<header className={styles.header}> <header
className={styles.header}
style={{ gridTemplateColumns: columnsTemplate }}
>
<Checkbox <Checkbox
checked={allItemsSelected} checked={allItemsSelected}
onChange={onSelectAllItems} onChange={onSelectAllItems}
@ -44,7 +48,6 @@ export function DataGridHeader<T>({
}); });
return ( return (
<RawButton <RawButton
style={{ flex: column.flex }}
className={cellClassName} className={cellClassName}
key={column.name} key={column.name}
onClick={() => handleSortButtonClick(column.name)} onClick={() => handleSortButtonClick(column.name)}

View File

@ -1,5 +1,5 @@
.header { .header {
display: flex; display: grid;
} }
.checkboxLabel { .checkboxLabel {
@ -13,7 +13,6 @@
position: relative; position: relative;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
flex: 1;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
border: solid 1px var(--clr-border-100); border: solid 1px var(--clr-border-100);

View File

@ -4,4 +4,5 @@ export type DataGridHeaderProps<T> = {
columns: DataGridColumnConfig<T>[]; columns: DataGridColumnConfig<T>[];
allItemsSelected: boolean; allItemsSelected: boolean;
onSelectAllItems: () => void; onSelectAllItems: () => void;
columnsTemplate: string;
}; };

View File

@ -10,20 +10,20 @@ export function DataGridRow<T>({
columns, columns,
selected, selected,
onSelect, onSelect,
columnsTemplate,
}: DataGridRowProps<T>) { }: DataGridRowProps<T>) {
return ( return (
<div className={styles.row}> <div
className={styles.row}
style={{ gridTemplateColumns: columnsTemplate }}
>
<Checkbox <Checkbox
checked={selected} checked={selected}
label={{ className: styles.checkboxLabel }} label={{ className: styles.checkboxLabel }}
onChange={onSelect} onChange={onSelect}
/> />
{columns.map((column) => ( {columns.map((column) => (
<div <div className={styles.cell} key={column.name}>
className={styles.cell}
style={{ flex: column.flex }}
key={column.name}
>
<Span>{column.getText(object)}</Span> <Span>{column.getText(object)}</Span>
</div> </div>
))} ))}

View File

@ -1,5 +1,5 @@
.row { .row {
display: flex; display: grid;
} }
.checkboxLabel { .checkboxLabel {
@ -11,7 +11,6 @@
.cell { .cell {
display: flex; display: flex;
overflow: hidden; overflow: hidden;
flex: 1 0 0;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
border: solid 1px var(--clr-border-100); border: solid 1px var(--clr-border-100);

View File

@ -5,4 +5,5 @@ export type DataGridRowProps<T> = {
columns: DataGridColumnConfig<T>[]; columns: DataGridColumnConfig<T>[];
selected: boolean; selected: boolean;
onSelect: () => void; onSelect: () => void;
columnsTemplate: string;
}; };

View File

@ -24,7 +24,7 @@ export function DataGridPreview() {
const columns: DataGridColumnConfig<Cat>[] = [ const columns: DataGridColumnConfig<Cat>[] = [
{ name: 'Name', getText: (cat) => cat.name }, { name: 'Name', getText: (cat) => cat.name },
{ name: 'Breed', getText: (cat) => cat.breed, scale: 2 }, { name: 'Breed', getText: (cat) => cat.breed, width: 2 },
{ name: 'Age', getText: (cat) => cat.age }, { name: 'Age', getText: (cat) => cat.age },
{ name: 'Color', getText: (cat) => cat.color }, { name: 'Color', getText: (cat) => cat.color },
]; ];

View File

@ -4,7 +4,7 @@ export type DataGridColumnConfig<T> = {
name: string; name: string;
getText: (object: T) => string; getText: (object: T) => string;
sortable?: boolean; sortable?: boolean;
flex?: string; width?: number;
}; };
export type DataGridSort = { export type DataGridSort = {

View File

@ -3,13 +3,14 @@ export { Button } from './button';
export { Checkbox } from './checkbox'; export { Checkbox } from './checkbox';
export { CheckboxGroup } from './checkbox-group'; export { CheckboxGroup } from './checkbox-group';
export { DateInput } from './date-input'; export { DateInput } from './date-input';
export { Dialog } from './dialog';
export { FileUploader } from './file-uploader'; export { FileUploader } from './file-uploader';
export { Heading } from './heading'; export { Heading } from './heading';
export { IconButton } from './icon-button'; export { IconButton } from './icon-button';
export { ImageFileManager } from './image-file-manager'; export { ImageFileManager } from './image-file-manager';
export { LinkButton } from './link-button'; export { LinkButton } from './link-button';
export { Menu } from './menu'; export { Menu } from './menu';
export { Dialog } from './dialog'; export { NumberField } from './number-field';
export { NumberInput } from './number-input'; export { NumberInput } from './number-input';
export { Overlay } from './overlay'; export { Overlay } from './overlay';
export { Pagination } from './pagination'; export { Pagination } from './pagination';

View File

@ -0,0 +1,28 @@
import React, { ForwardedRef, forwardRef } from 'react';
import { Label, LabelProps } from '../label';
import { NumberInput } from '../number-input';
import { NumberFieldProps } from './types';
function NumberFieldInner(
{ scale, label = {}, required, ...props }: Omit<NumberFieldProps, 'ref'>,
ref: ForwardedRef<HTMLInputElement>,
) {
const labelProps: LabelProps = {
...label,
required: { value: required, ...label.required },
};
return (
<Label scale={scale} {...labelProps}>
<NumberInput
scale={scale}
ref={ref}
required={required}
{...props}
invalid={label.error !== undefined}
/>
</Label>
);
}
export const NumberField = forwardRef(NumberFieldInner);

View File

@ -0,0 +1,6 @@
import { LabelProps } from '../label/types';
import { NumberInputProps } from '../number-input/types';
export type NumberFieldProps = {
label?: LabelProps;
} & NumberInputProps;

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { TextInput } from '../text-input'; import { Input } from '../input';
import { NumberInputProps } from './types'; import { NumberInputProps } from './types';
export function NumberInput({ export function NumberInput({
@ -34,5 +34,5 @@ export function NumberInput({
onChange?.(num); onChange?.(num);
}; };
return <TextInput value={value ?? ''} onChange={handleChange} {...props} />; return <Input value={value ?? ''} onChange={handleChange} {...props} />;
} }

View File

@ -1,5 +1,5 @@
export { Header } from './header'; export { Header } from './header';
export { ParkTurbineTable } from './park-turbine-table';
export { ParkTurbines } from './park-turbines';
export { Sidebar } from './sidebar'; export { Sidebar } from './sidebar';
export { SignInForm } from './sign-in-form';
export { SignUpForm } from './sign-up-form';
export { ThemeSelect } from './theme-select'; export { ThemeSelect } from './theme-select';

View File

@ -0,0 +1,35 @@
import { ParkFormTurbine } from '@components/pages/park-page/types';
import React from 'react';
import { ParkTurbineTableHeader, ParkTurbineTableRow } from './components';
import { ParkTurbineTableProps } from './types';
export function ParkTurbineTable({
turbines,
onChange,
selectedIDs,
onSelect,
}: ParkTurbineTableProps) {
const handleRowChange = (turbine: ParkFormTurbine, index: number) => {
onChange(turbines.with(index, turbine));
};
const handleSelect = (turbineId: number) => {
onSelect({ ...selectedIDs, [turbineId]: !selectedIDs[turbineId] });
};
return (
<div>
<ParkTurbineTableHeader />
{turbines.map((turbine, index) => (
<ParkTurbineTableRow
turbine={turbine}
onChange={(t) => handleRowChange(t, index)}
selected={selectedIDs[turbine.id] ?? false}
onSelect={() => handleSelect(turbine.id)}
key={turbine.id}
/>
))}
</div>
);
}

View File

@ -0,0 +1,2 @@
export * from './park-turbine-table-header';
export * from './park-turbine-table-row';

View File

@ -0,0 +1,35 @@
import { Checkbox } from '@components/ui/checkbox';
import { Span } from '@components/ui/span';
import React from 'react';
import styles from './styles.module.scss';
export function ParkTurbineTableHeader() {
return (
<header className={styles.header}>
<Checkbox
checked={false}
onChange={() => {}}
label={{ className: styles.checkboxLabel }}
/>
<Span color="t300" className={styles.cell}>
Id
</Span>
<Span color="t300" className={styles.cell}>
Name
</Span>
<Span color="t300" className={styles.cell}>
X
</Span>
<Span color="t300" className={styles.cell}>
Y
</Span>
<Span color="t300" className={styles.cell}>
Angle
</Span>
<Span color="t300" className={styles.cell}>
Comment
</Span>
</header>
);
}

View File

@ -0,0 +1 @@
export * from './component';

View File

@ -0,0 +1,26 @@
.header {
display: grid;
grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr 3fr;
}
.checkboxLabel {
padding: 10px;
border: solid 1px var(--clr-border-100);
background-color: var(--clr-layer-300);
border-top-left-radius: 10px;
}
.cell {
position: relative;
display: flex;
overflow: hidden;
align-items: center;
padding: 10px;
border: solid 1px var(--clr-border-100);
background-color: var(--clr-layer-300);
transition: all var(--td-100) ease-in-out;
&:last-of-type {
border-top-right-radius: 10px;
}
}

View File

@ -0,0 +1,52 @@
import { NumberInput } from '@components/ui';
import { Checkbox } from '@components/ui/checkbox';
import { Input } from '@components/ui/input';
import React from 'react';
import styles from './styles.module.scss';
import { ParkTurbineTableRowProps } from './types';
export function ParkTurbineTableRow({
turbine,
onChange,
selected,
onSelect,
}: ParkTurbineTableRowProps) {
return (
<div className={styles.row}>
<Checkbox
checked={selected}
onChange={onSelect}
label={{ className: styles.checkboxLabel }}
/>
<Input value={turbine.id} wrapper={{ className: styles.cell }} disabled />
<Input
value={turbine.name}
wrapper={{ className: styles.cell }}
disabled
/>
<NumberInput
value={String(turbine.xOffset)}
wrapper={{ className: styles.cell }}
onChange={(value) => onChange({ ...turbine, xOffset: value })}
/>
<NumberInput
value={String(turbine.yOffset)}
wrapper={{ className: styles.cell }}
onChange={(value) => onChange({ ...turbine, yOffset: value })}
/>
<NumberInput
value={String(turbine.angle)}
wrapper={{ className: styles.cell }}
onChange={(value) => onChange({ ...turbine, angle: value })}
/>
<Input
value={turbine.comment}
wrapper={{ className: styles.cell }}
onChange={(event) =>
onChange({ ...turbine, comment: event.target.value })
}
/>
</div>
);
}

View File

@ -0,0 +1 @@
export * from './component';

View File

@ -0,0 +1,16 @@
.row {
display: grid;
grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr 3fr;
.cell {
border: solid 1px var(--clr-border-100);
border-radius: 0;
background-color: var(--clr-layer-200);
}
}
.checkboxLabel {
padding: 10px;
border: solid 1px var(--clr-border-100);
background-color: var(--clr-layer-200);
}

View File

@ -0,0 +1,8 @@
import { ParkFormTurbine } from '@components/pages/park-page/types';
export type ParkTurbineTableRowProps = {
turbine: ParkFormTurbine;
onChange: (turbine: ParkFormTurbine) => void;
selected: boolean;
onSelect: () => void;
};

View File

@ -0,0 +1 @@
export * from './component';

View File

@ -0,0 +1,8 @@
import { ParkFormTurbine } from '@components/pages/park-page/types';
export type ParkTurbineTableProps = {
turbines: ParkFormTurbine[];
onChange: (turbines: ParkFormTurbine[]) => void;
selectedIDs: Record<number, boolean>;
onSelect: (selected: Record<number, boolean>) => void;
};

View File

@ -0,0 +1,87 @@
import { getTurbineTypes, TurbineType } from '@api/wind';
import { ParkFormTurbine } from '@components/pages/park-page/types';
import { Autocomplete, Button } from '@components/ui';
import React, { useEffect, useState } from 'react';
import { ParkTurbineTable } from '../park-turbine-table';
import styles from './styles.module.scss';
import { ParkTurbinesProps } from './types';
export function ParkTurbines({
savedTurbines,
value = [],
onChange,
}: ParkTurbinesProps) {
const [turbineTypes, setTurbineTypes] = useState<TurbineType[]>([]);
const [turbineType, setTurbineType] = useState<TurbineType>(null);
const [selectedIDs, setSelectedIDs] = useState<Record<number, boolean>>({});
const fetchTurbineTypes = async () => {
const res = await getTurbineTypes();
setTurbineTypes(res.data ?? []);
};
useEffect(() => {
fetchTurbineTypes();
}, []);
const handleAddButtonClick = () => {
if (
savedTurbines.find((t) => t.id === turbineType.id) ||
value.find((t) => t.id === turbineType.id)
) {
return;
}
onChange([
...value,
{
id: turbineType.id,
name: turbineType.name,
xOffset: '',
yOffset: '',
angle: '',
comment: '',
new: true,
},
]);
};
const handleDeleteButtonClick = () => {
const newValue: ParkFormTurbine[] = [];
value.forEach((turbine) => {
if (!selectedIDs[turbine.id]) {
newValue.push(turbine);
return;
}
if (savedTurbines.find((t) => t.id === turbine.id)) {
newValue.push({ ...turbine, delete: true });
}
});
onChange(newValue);
setSelectedIDs({});
};
return (
<div className={styles.parkTurbines}>
<div className={styles.actions}>
<Autocomplete
options={turbineTypes ?? []}
getOptionKey={({ id }) => id}
getOptionLabel={({ name }) => name}
onChange={(t) => setTurbineType(t)}
value={turbineType}
/>
<Button onClick={handleAddButtonClick}>Add</Button>
<Button variant="secondary" onClick={handleDeleteButtonClick}>
Delete
</Button>
</div>
<ParkTurbineTable
turbines={value.filter((t) => !t.delete)}
onChange={onChange}
selectedIDs={selectedIDs}
onSelect={setSelectedIDs}
/>
</div>
);
}

View File

@ -0,0 +1 @@
export * from './component';

View File

@ -0,0 +1,14 @@
.parkTurbines {
display: grid;
gap: 10px;
grid-template-rows: auto 1fr;
}
.actions {
display: flex;
padding: 10px;
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);
gap: 10px;
}

View File

@ -0,0 +1,8 @@
import { ParkTurbine } from '@api/wind';
import { ParkFormTurbine } from '@components/pages/park-page/types';
export type ParkTurbinesProps = {
savedTurbines: ParkTurbine[];
value?: ParkFormTurbine[];
onChange?: (value: ParkFormTurbine[]) => void;
};

View File

@ -1,40 +0,0 @@
import {
Button,
Heading,
LinkButton,
PasswordInput,
TextInput,
} from '@components/ui';
import { useForm } from '@utils/form';
import { ROUTES } from '@utils/route';
import clsx from 'clsx';
import React from 'react';
import styles from './styles.module.scss';
import { SignInFormProps, SignInFormStore } from './types';
export function SignInForm({ className, ...props }: SignInFormProps) {
const { register, getValues } = useForm<SignInFormStore>({});
const classNames = clsx(className, styles.form);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
console.log(getValues());
};
return (
<form onSubmit={handleSubmit} className={classNames} {...props}>
<Heading tag="h1" className={styles.heading}>
Sign in
</Heading>
<div className={styles.inputBox}>
<TextInput {...register('email')} label={{ text: 'Email' }} />
<PasswordInput {...register('password')} label={{ text: 'Password' }} />
</div>
<div className={styles.buttonBox}>
<Button type="submit">Sign in</Button>
<LinkButton href={ROUTES.signUp.path}>Not a member?</LinkButton>
</div>
</form>
);
}

View File

@ -1 +0,0 @@
export { SignInForm } from './component';

View File

@ -1,26 +0,0 @@
.form {
display: grid;
padding: 40px 20px 20px;
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);
gap: 30px;
& > * {
width: 100%;
}
}
.heading {
text-align: center;
}
.inputBox {
display: grid;
gap: 20px;
}
.buttonBox {
display: grid;
gap: 10px;
}

View File

@ -1,8 +0,0 @@
import { ComponentProps } from 'react';
export type SignInFormStore = {
email: string;
password: string;
};
export type SignInFormProps = {} & ComponentProps<'form'>;

View File

@ -1,40 +0,0 @@
import {
Button,
Heading,
LinkButton,
PasswordInput,
TextInput,
} from '@components/ui';
import { useForm } from '@utils/form';
import { ROUTES } from '@utils/route';
import clsx from 'clsx';
import React from 'react';
import styles from './styles.module.scss';
import { SignUpFormProps, SignUpFormStore } from './types';
export function SignUpForm({ className, ...props }: SignUpFormProps) {
const { register, getValues } = useForm<SignUpFormStore>({});
const classNames = clsx(className, styles.form);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
console.log(getValues());
};
return (
<form onSubmit={handleSubmit} className={classNames} {...props}>
<Heading tag="h1" className={styles.heading}>
Sign up
</Heading>
<div className={styles.inputBox}>
<TextInput {...register('email')} label={{ text: 'Email' }} />
<PasswordInput {...register('password')} label={{ text: 'Password' }} />
</div>
<div className={styles.buttonBox}>
<Button type="submit">Sign up</Button>
<LinkButton href={ROUTES.signIn.path}>Already a member?</LinkButton>
</div>
</form>
);
}

View File

@ -1 +0,0 @@
export { SignUpForm } from './component';

View File

@ -1,26 +0,0 @@
.form {
display: grid;
padding: 40px 20px 20px;
border-radius: 15px;
background-color: var(--clr-layer-200);
box-shadow: 0px 1px 2px var(--clr-shadow-100);
gap: 30px;
& > * {
width: 100%;
}
}
.heading {
text-align: center;
}
.inputBox {
display: grid;
gap: 20px;
}
.buttonBox {
display: grid;
gap: 10px;
}

View File

@ -1,9 +0,0 @@
import { ComponentProps } from 'react';
export type SignUpFormStore = {
email: string;
password: string;
passwordRepeat: string;
};
export type SignUpFormProps = {} & ComponentProps<'form'>;

View File

@ -143,12 +143,12 @@ class WindParkTurbineRepository:
return db_park_turbine return db_park_turbine
@staticmethod @staticmethod
def get(db: Session, park_turbine_id: int): def get(db: Session, park_id: int, turbine_id: int):
return db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first() return db.query(WindParkTurbine).filter(WindParkTurbine.wind_park_id == park_id, WindParkTurbine.turbine_id == turbine_id).first()
@staticmethod @staticmethod
def update(db: Session, park_turbine_id: int, park_turbine: WindParkTurbineCreate): def update(db: Session, park_id: int, turbine_id: int, park_turbine: WindParkTurbineCreate):
db_park_turbine = db.query(WindParkTurbine).filter(WindParkTurbine.turbine_id == park_turbine_id).first() db_park_turbine = db.query(WindParkTurbine).filter(WindParkTurbine.wind_park_id == park_id, WindParkTurbine.turbine_id == turbine_id).first()
if db_park_turbine: if db_park_turbine:
for key, value in park_turbine.dict().items(): for key, value in park_turbine.dict().items():
setattr(db_park_turbine, key, value) setattr(db_park_turbine, key, value)

View File

@ -77,17 +77,17 @@ async def create_park_turbine(park_turbine: WindParkTurbineCreate, db: Session =
return WindParkTurbineRepository.create(db=db, park_turbine=park_turbine) return WindParkTurbineRepository.create(db=db, park_turbine=park_turbine)
@router.get("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse) @router.get("/park_turbine/{park_id}/{turbine_id}", response_model=WindParkTurbineResponse)
async def read_park_turbine(park_turbine_id: int, db: Session = Depends(get_db)): async def read_park_turbine(park_id: int, turbine_id: int, db: Session = Depends(get_db)):
park_turbine = WindParkTurbineRepository.get(db=db, park_turbine_id=park_turbine_id) park_turbine = WindParkTurbineRepository.get(db=db, park_id=park_id, turbine_id=turbine_id)
if park_turbine is None: if park_turbine is None:
raise HTTPException(status_code=404, detail="Park Turbine not found") raise HTTPException(status_code=404, detail="Park Turbine not found")
return park_turbine return park_turbine
@router.put("/park_turbine/{park_turbine_id}", response_model=WindParkTurbineResponse) @router.put("/park_turbine/{park_id}/{turbine_id}", response_model=WindParkTurbineResponse)
async def update_park_turbine(park_turbine_id: int, park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)): async def update_park_turbine(park_id: int, turbine_id: int, park_turbine: WindParkTurbineCreate, db: Session = Depends(get_db)):
updated_park_turbine = WindParkTurbineRepository.update(db=db, park_turbine_id=park_turbine_id, updated_park_turbine = WindParkTurbineRepository.update(db=db, park_id=park_id, turbine_id=turbine_id,
park_turbine=park_turbine) park_turbine=park_turbine)
if updated_park_turbine is None: if updated_park_turbine is None:
raise HTTPException(status_code=404, detail="Park Turbine not found") raise HTTPException(status_code=404, detail="Park Turbine not found")