forked from jappel/leistungsbilanz-ts
Add estimate row project device handoff
This commit is contained in:
parent
8b1923e8ec
commit
a6500e2b26
9 changed files with 434 additions and 21 deletions
|
|
@ -2141,7 +2141,7 @@ button.structure-entry-main:focus-visible {
|
|||
.estimate-col-result { width: 4.6rem; }
|
||||
.estimate-col-sv { width: 3.2rem; }
|
||||
.estimate-col-remark { width: 9rem; }
|
||||
.estimate-col-actions { width: 2.4rem; }
|
||||
.estimate-col-actions { width: 4.5rem; }
|
||||
|
||||
.estimate-cell-input,
|
||||
.estimate-cell-select {
|
||||
|
|
@ -2286,6 +2286,13 @@ button.structure-entry-main:focus-visible {
|
|||
gap: 2px;
|
||||
}
|
||||
|
||||
.estimate-row-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.estimate-group-title-row .estimate-actions-cell {
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ import {
|
|||
} from "lucide-react";
|
||||
import {
|
||||
ApiRequestError,
|
||||
createProjectDevice,
|
||||
getEstimateBalance,
|
||||
getEstimateComparison,
|
||||
getProject,
|
||||
getProjectHistory,
|
||||
isRevisionConflictError,
|
||||
listProjectDevices,
|
||||
redoProjectCommand,
|
||||
undoProjectCommand,
|
||||
updateEstimateBalance,
|
||||
|
|
@ -26,11 +28,17 @@ import {
|
|||
import type {
|
||||
EstimateBalanceDto,
|
||||
EstimateComparisonDto,
|
||||
CreateProjectDeviceInput,
|
||||
ProjectDto,
|
||||
ProjectDeviceDto,
|
||||
ProjectHistoryStateDto,
|
||||
} from "../../../../../frontend/types";
|
||||
import type { EstimateGroupSnapshot } from "../../../../../shared/estimate-balance/estimate-balance";
|
||||
import type {
|
||||
EstimateGroupSnapshot,
|
||||
EstimateRowSnapshot,
|
||||
} from "../../../../../shared/estimate-balance/estimate-balance";
|
||||
import { GzfPickerModal } from "../../../../../frontend/components/gzf-picker-modal";
|
||||
import { ProjectDeviceModal } from "../../../../../frontend/components/project-device-modal";
|
||||
import {
|
||||
addEstimateGroup,
|
||||
estimateBalanceFingerprint,
|
||||
|
|
@ -41,7 +49,11 @@ import {
|
|||
updateEstimateRow,
|
||||
validateEstimateBalance,
|
||||
} from "../../../../../frontend/estimate-balance/estimate-balance-document";
|
||||
import { formatIsoDateGerman } from "../../../../../frontend/estimate-balance/estimate-balance-format";
|
||||
import {
|
||||
formatGermanNumber,
|
||||
formatIsoDateGerman,
|
||||
} from "../../../../../frontend/estimate-balance/estimate-balance-format";
|
||||
import { createEstimateProjectDeviceInitialValues } from "../../../../../frontend/estimate-balance/estimate-project-device-prefill";
|
||||
import {
|
||||
EstimateBalanceSheet,
|
||||
type EstimateBalanceUpdate,
|
||||
|
|
@ -60,6 +72,10 @@ const CONFLICT_MESSAGE = "Die Bilanz wurde zwischenzeitlich geändert und neu ge
|
|||
type SaveStatus = "saved" | "pending" | "saving" | "error" | "invalid";
|
||||
|
||||
type GroupDialog = { group: EstimateGroupSnapshot | null };
|
||||
type ProjectDeviceDialog = {
|
||||
group: EstimateGroupSnapshot;
|
||||
row: EstimateRowSnapshot;
|
||||
};
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
return error instanceof Error && error.message ? error.message : fallback;
|
||||
|
|
@ -94,6 +110,11 @@ export default function EstimateBalanceEditorPage() {
|
|||
const [gzfTarget, setGzfTarget] = useState<{ groupId: string; rowId: string } | null>(
|
||||
null
|
||||
);
|
||||
const [projectDeviceDialog, setProjectDeviceDialog] =
|
||||
useState<ProjectDeviceDialog | null>(null);
|
||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||
const [isPreparingProjectDevice, setIsPreparingProjectDevice] = useState(false);
|
||||
const [isSavingProjectDevice, setIsSavingProjectDevice] = useState(false);
|
||||
|
||||
// Refs carry the latest values into timers, unload handlers and chained
|
||||
// async saves without re-creating them.
|
||||
|
|
@ -456,7 +477,77 @@ export default function EstimateBalanceEditorPage() {
|
|||
setGzfTarget(null);
|
||||
}
|
||||
|
||||
const isEditingDisabled = !draft || isHistoryBusy;
|
||||
async function handleOpenProjectDevice(groupId: string, rowId: string) {
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
setIsPreparingProjectDevice(true);
|
||||
try {
|
||||
if (!(await saveNow())) {
|
||||
setError(
|
||||
"Die Grobbilanz muss zuerst fehlerfrei gespeichert werden, bevor ein Projektgerät angelegt werden kann."
|
||||
);
|
||||
return;
|
||||
}
|
||||
const current = draftRef.current;
|
||||
const group = current?.groups.find((entry) => entry.id === groupId);
|
||||
const row = group?.rows.find((entry) => entry.id === rowId);
|
||||
if (!group || !row) {
|
||||
setError("Die gewählte Grobbilanz-Zeile ist nicht mehr vorhanden.");
|
||||
return;
|
||||
}
|
||||
const existingDevices = await listProjectDevices(projectId);
|
||||
setProjectDevices(existingDevices);
|
||||
setProjectDeviceDialog({ group, row });
|
||||
} catch (err) {
|
||||
setError(
|
||||
errorMessage(err, "Der Projektgeräte-Dialog konnte nicht vorbereitet werden.")
|
||||
);
|
||||
} finally {
|
||||
setIsPreparingProjectDevice(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateProjectDevice(input: CreateProjectDeviceInput) {
|
||||
const revision = revisionRef.current;
|
||||
if (!projectDeviceDialog || revision === null) return;
|
||||
setError(null);
|
||||
setIsSavingProjectDevice(true);
|
||||
try {
|
||||
if (!(await saveNow())) {
|
||||
setError(
|
||||
"Die Grobbilanz muss zuerst fehlerfrei gespeichert werden, bevor das Projektgerät angelegt werden kann."
|
||||
);
|
||||
return;
|
||||
}
|
||||
const currentRevision = revisionRef.current;
|
||||
if (currentRevision === null) return;
|
||||
const result = await createProjectDevice(projectId, input, currentRevision);
|
||||
revisionRef.current = result.history.currentRevision;
|
||||
setHistory(result.history);
|
||||
setProject((currentProject) =>
|
||||
currentProject
|
||||
? { ...currentProject, currentRevision: result.history.currentRevision }
|
||||
: currentProject
|
||||
);
|
||||
setProjectDevices((current) => [...current, result.projectDevice]);
|
||||
setProjectDeviceDialog(null);
|
||||
setNotice(`Projektgerät „${result.projectDevice.displayName}“ wurde angelegt.`);
|
||||
} catch (err) {
|
||||
if (isRevisionConflictError(err)) {
|
||||
await reloadAll().catch(handleReloadError);
|
||||
setError(
|
||||
"Der Projektstand hat sich zwischenzeitlich geändert. Der aktuelle Stand wurde geladen; bitte das Projektgerät erneut speichern."
|
||||
);
|
||||
} else {
|
||||
setError(errorMessage(err, "Das Projektgerät konnte nicht angelegt werden."));
|
||||
}
|
||||
} finally {
|
||||
setIsSavingProjectDevice(false);
|
||||
}
|
||||
}
|
||||
|
||||
const isEditingDisabled =
|
||||
!draft || isHistoryBusy || isPreparingProjectDevice || isSavingProjectDevice;
|
||||
const invalidCount = invalidFieldKeys.size;
|
||||
const listHref = `/projects/${projectId}/grobbilanz`;
|
||||
|
||||
|
|
@ -623,6 +714,9 @@ export default function EstimateBalanceEditorPage() {
|
|||
onChange={handleChange}
|
||||
onEditGroup={(group) => setGroupDialog({ group })}
|
||||
onOpenGzfPicker={(groupId, rowId) => setGzfTarget({ groupId, rowId })}
|
||||
onTakeOverRow={(groupId, rowId) =>
|
||||
void handleOpenProjectDevice(groupId, rowId)
|
||||
}
|
||||
onValidityChange={handleValidityChange}
|
||||
threePhaseVoltageV={project.threePhaseVoltageV}
|
||||
/>
|
||||
|
|
@ -692,10 +786,67 @@ export default function EstimateBalanceEditorPage() {
|
|||
{gzfTarget ? (
|
||||
<GzfPickerModal onClose={() => setGzfTarget(null)} onSelect={handleSelectGzf} />
|
||||
) : null}
|
||||
{projectDeviceDialog ? (
|
||||
<ProjectDeviceModal
|
||||
context={
|
||||
<EstimateProjectDeviceContext
|
||||
balanceName={draft?.name ?? ""}
|
||||
group={projectDeviceDialog.group}
|
||||
row={projectDeviceDialog.row}
|
||||
/>
|
||||
}
|
||||
existingDevices={projectDevices}
|
||||
globalDevices={[]}
|
||||
initialValues={createEstimateProjectDeviceInitialValues(
|
||||
projectDeviceDialog.group,
|
||||
projectDeviceDialog.row
|
||||
)}
|
||||
isSaving={isSavingProjectDevice}
|
||||
onClose={() => setProjectDeviceDialog(null)}
|
||||
onImportGlobal={async () => undefined}
|
||||
onSave={handleCreateProjectDevice}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function EstimateProjectDeviceContext({
|
||||
balanceName,
|
||||
group,
|
||||
row,
|
||||
}: {
|
||||
balanceName: string;
|
||||
group: EstimateGroupSnapshot;
|
||||
row: EstimateRowSnapshot;
|
||||
}) {
|
||||
const originalUnit = row.unit === "area" ? "m²" : "St.";
|
||||
const powerUnit = row.unit === "area" ? "W/m²" : "W";
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<strong>Herkunft:</strong> Stand „{balanceName}“, Kostengruppe{" "}
|
||||
{group.costGroup || "ohne Nummer"}.
|
||||
</div>
|
||||
<div>
|
||||
Originalwerte: {formatGermanNumber(row.quantity, 0, 3)} {originalUnit} ×{" "}
|
||||
{formatGermanNumber(row.powerPerUnitW, 0, 3)} {powerUnit}, GZF{" "}
|
||||
{formatGermanNumber(row.simultaneityFactor, 0, 3)}, cos φ{" "}
|
||||
{formatGermanNumber(row.cosPhi, 0, 3)}.
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
{row.unit === "area"
|
||||
? "Das Projektgerät dient als Vorlage. Für Flächenwerte sind Anzahl und Leistung je Stück manuell einzutragen."
|
||||
: "Das Projektgerät ist eine Vorlage mit Anzahl 1."}{" "}
|
||||
Kategorie bitte prüfen.
|
||||
{row.safetySupply
|
||||
? " SV ist nur ein Hinweis; die Versorgungsart wird an der Verteilung festgelegt."
|
||||
: ""}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SaveStatusBadge({
|
||||
error,
|
||||
invalidCount,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { FormEvent, useState } from "react";
|
||||
import React, { FormEvent, type ReactNode, useState } from "react";
|
||||
import type {
|
||||
CreateProjectDeviceInput,
|
||||
GlobalDeviceDto,
|
||||
|
|
@ -14,10 +14,20 @@ import {
|
|||
} from "../../shared/constants/circuit-group";
|
||||
import { GzfPickerModal } from "./gzf-picker-modal";
|
||||
|
||||
export type ProjectDeviceModalInitialValues = Omit<
|
||||
Partial<CreateProjectDeviceInput>,
|
||||
"quantity" | "powerPerUnit"
|
||||
> & {
|
||||
quantity?: number | null;
|
||||
powerPerUnit?: number | null;
|
||||
};
|
||||
|
||||
interface ProjectDeviceModalProps {
|
||||
context?: ReactNode;
|
||||
existingDevices?: ReadonlyArray<Pick<ProjectDeviceDto, "name" | "displayName">>;
|
||||
globalDevices: GlobalDeviceDto[];
|
||||
initialDevice?: ProjectDeviceDto;
|
||||
initialValues?: Partial<CreateProjectDeviceInput>;
|
||||
initialValues?: ProjectDeviceModalInitialValues;
|
||||
isSaving: boolean;
|
||||
onClose: () => void;
|
||||
onImportGlobal: (globalDeviceId: string) => Promise<void>;
|
||||
|
|
@ -25,6 +35,8 @@ interface ProjectDeviceModalProps {
|
|||
}
|
||||
|
||||
export function ProjectDeviceModal({
|
||||
context,
|
||||
existingDevices = [],
|
||||
globalDevices,
|
||||
initialDevice,
|
||||
initialValues,
|
||||
|
|
@ -36,6 +48,11 @@ export function ProjectDeviceModal({
|
|||
const [values, setValues] = useState(() => toFormValues(initialDevice, initialValues));
|
||||
const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState("");
|
||||
const [isGzfPickerOpen, setIsGzfPickerOpen] = useState(false);
|
||||
const hasDuplicateName = hasExistingProjectDeviceName(
|
||||
values.name,
|
||||
values.displayName,
|
||||
existingDevices
|
||||
);
|
||||
|
||||
function update(key: keyof typeof values, value: string) {
|
||||
setValues((current) => ({ ...current, [key]: value }));
|
||||
|
|
@ -79,6 +96,17 @@ export function ProjectDeviceModal({
|
|||
submitLabel={initialDevice ? "Änderungen speichern" : "Gerät anlegen"}
|
||||
title={initialDevice ? "Projektgerät bearbeiten" : "Projektgerät hinzufügen"}
|
||||
>
|
||||
{context ? (
|
||||
<div className="alert alert-info small" role="note">
|
||||
{context}
|
||||
</div>
|
||||
) : null}
|
||||
{hasDuplicateName ? (
|
||||
<div className="alert alert-warning small" role="status">
|
||||
Ein Projektgerät mit gleichem internen Namen oder Anzeigenamen ist bereits vorhanden.
|
||||
Das Anlegen bleibt möglich.
|
||||
</div>
|
||||
) : null}
|
||||
{!initialDevice && globalDevices.length > 0 ? (
|
||||
<div className="border rounded p-3 mb-4">
|
||||
<label className="form-label" htmlFor="global-project-device">
|
||||
|
|
@ -287,7 +315,7 @@ function NumberField({
|
|||
|
||||
function toFormValues(
|
||||
device?: ProjectDeviceDto,
|
||||
initialValues?: Partial<CreateProjectDeviceInput>
|
||||
initialValues?: ProjectDeviceModalInitialValues
|
||||
) {
|
||||
return {
|
||||
name: device?.name ?? initialValues?.name ?? "",
|
||||
|
|
@ -295,14 +323,24 @@ function toFormValues(
|
|||
connectionKind: device?.connectionKind ?? initialValues?.connectionKind ?? "",
|
||||
costGroup: device?.costGroup ?? initialValues?.costGroup ?? "",
|
||||
category: device?.category ?? initialValues?.category ?? "single_phase",
|
||||
quantity: String(device?.quantity ?? initialValues?.quantity ?? 1),
|
||||
powerPerUnit: String(device?.powerPerUnit ?? initialValues?.powerPerUnit ?? 0.1),
|
||||
quantity: toInitialNumber(device?.quantity, initialValues?.quantity, 1),
|
||||
powerPerUnit: toInitialNumber(device?.powerPerUnit, initialValues?.powerPerUnit, 0.1),
|
||||
simultaneityFactor: String(device?.simultaneityFactor ?? initialValues?.simultaneityFactor ?? 1),
|
||||
cosPhi: String(device?.cosPhi ?? initialValues?.cosPhi ?? 1),
|
||||
remark: device?.remark ?? initialValues?.remark ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function toInitialNumber(
|
||||
deviceValue: number | undefined,
|
||||
initialValue: number | null | undefined,
|
||||
fallback: number
|
||||
) {
|
||||
if (deviceValue !== undefined) return String(deviceValue);
|
||||
if (initialValue === null) return "";
|
||||
return String(initialValue ?? fallback);
|
||||
}
|
||||
|
||||
function optionalString(value: string) {
|
||||
return value.trim() || undefined;
|
||||
}
|
||||
|
|
@ -315,7 +353,29 @@ function parseDecimal(value: string) {
|
|||
return Number(value.trim().replace(",", "."));
|
||||
}
|
||||
|
||||
function isNumberInRange(value: string, min: number, max = Number.POSITIVE_INFINITY) {
|
||||
export function isProjectDeviceNumberInRange(
|
||||
value: string,
|
||||
min: number,
|
||||
max = Number.POSITIVE_INFINITY
|
||||
) {
|
||||
if (!value.trim()) return false;
|
||||
const parsed = parseDecimal(value);
|
||||
return Number.isFinite(parsed) && parsed >= min && parsed <= max;
|
||||
}
|
||||
|
||||
const isNumberInRange = isProjectDeviceNumberInRange;
|
||||
|
||||
export function hasExistingProjectDeviceName(
|
||||
name: string,
|
||||
displayName: string,
|
||||
existingDevices: ReadonlyArray<Pick<ProjectDeviceDto, "name" | "displayName">>
|
||||
) {
|
||||
const candidates = new Set(
|
||||
[name, displayName].map((value) => value.trim().toLocaleLowerCase("de-DE")).filter(Boolean)
|
||||
);
|
||||
return existingDevices.some((device) =>
|
||||
[device.name, device.displayName].some((value) =>
|
||||
candidates.has(value.trim().toLocaleLowerCase("de-DE"))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { ArrowDown, ArrowUp, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import { ArrowDown, ArrowUp, PackagePlus, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import {
|
||||
calculateEstimateBalance,
|
||||
calculateEstimateRow,
|
||||
|
|
@ -46,6 +46,7 @@ interface EstimateBalanceSheetProps {
|
|||
onChange: EstimateBalanceUpdate;
|
||||
onEditGroup: (group: EstimateGroupSnapshot) => void;
|
||||
onOpenGzfPicker: (groupId: string, rowId: string) => void;
|
||||
onTakeOverRow: (groupId: string, rowId: string) => void;
|
||||
onValidityChange: (fieldKey: string, isValid: boolean) => void;
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
|
|
@ -178,6 +179,7 @@ function EstimateRowLine({
|
|||
group,
|
||||
onChange,
|
||||
onOpenGzfPicker,
|
||||
onTakeOverRow,
|
||||
onValidityChange,
|
||||
row,
|
||||
rowIndex,
|
||||
|
|
@ -186,6 +188,7 @@ function EstimateRowLine({
|
|||
group: EstimateGroupSnapshot;
|
||||
onChange: EstimateBalanceUpdate;
|
||||
onOpenGzfPicker: (groupId: string, rowId: string) => void;
|
||||
onTakeOverRow: (groupId: string, rowId: string) => void;
|
||||
onValidityChange: (fieldKey: string, isValid: boolean) => void;
|
||||
row: EstimateRowSnapshot;
|
||||
rowIndex: number;
|
||||
|
|
@ -348,14 +351,24 @@ function EstimateRowLine({
|
|||
/>
|
||||
</td>
|
||||
<td className="estimate-actions-cell estimate-no-print">
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
label={`Zeile löschen (${rowLabel})`}
|
||||
onClick={() => onChange((balance) => removeEstimateRow(balance, group.id, row.id))}
|
||||
variant="danger"
|
||||
>
|
||||
<Trash2 aria-hidden="true" size={13} />
|
||||
</IconButton>
|
||||
<div className="estimate-row-actions">
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
label={`Als Projektgerät übernehmen (${rowLabel})`}
|
||||
onClick={() => onTakeOverRow(group.id, row.id)}
|
||||
variant="primary"
|
||||
>
|
||||
<PackagePlus aria-hidden="true" size={13} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
label={`Zeile löschen (${rowLabel})`}
|
||||
onClick={() => onChange((balance) => removeEstimateRow(balance, group.id, row.id))}
|
||||
variant="danger"
|
||||
>
|
||||
<Trash2 aria-hidden="true" size={13} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
@ -373,6 +386,7 @@ export function EstimateBalanceSheet({
|
|||
onChange,
|
||||
onEditGroup,
|
||||
onOpenGzfPicker,
|
||||
onTakeOverRow,
|
||||
onValidityChange,
|
||||
threePhaseVoltageV,
|
||||
}: EstimateBalanceSheetProps) {
|
||||
|
|
@ -546,6 +560,7 @@ export function EstimateBalanceSheet({
|
|||
key={row.id}
|
||||
onChange={onChange}
|
||||
onOpenGzfPicker={onOpenGzfPicker}
|
||||
onTakeOverRow={onTakeOverRow}
|
||||
onValidityChange={onValidityChange}
|
||||
row={row}
|
||||
rowIndex={rowIndex}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import type { CreateProjectDeviceInput } from "../types";
|
||||
import type {
|
||||
EstimateGroupSnapshot,
|
||||
EstimateRowSnapshot,
|
||||
} from "../../shared/estimate-balance/estimate-balance";
|
||||
|
||||
export type EstimateProjectDeviceInitialValues = Omit<
|
||||
Partial<CreateProjectDeviceInput>,
|
||||
"quantity" | "powerPerUnit"
|
||||
> & {
|
||||
quantity?: number | null;
|
||||
powerPerUnit?: number | null;
|
||||
};
|
||||
|
||||
const connectionLabels = {
|
||||
fixed: "Festanschluss",
|
||||
socket: "Steckdose",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Builds the editable ProjectDevice proposal for one estimate row. Area rows
|
||||
* deliberately leave quantity and power blank because W/m² has no equivalent
|
||||
* in the current ProjectDevice model.
|
||||
*/
|
||||
export function createEstimateProjectDeviceInitialValues(
|
||||
group: Pick<EstimateGroupSnapshot, "costGroup">,
|
||||
row: Pick<
|
||||
EstimateRowSnapshot,
|
||||
| "name"
|
||||
| "unit"
|
||||
| "connection"
|
||||
| "powerPerUnitW"
|
||||
| "simultaneityFactor"
|
||||
| "cosPhi"
|
||||
| "remark"
|
||||
>
|
||||
): EstimateProjectDeviceInitialValues {
|
||||
const name = row.name.trim();
|
||||
return {
|
||||
name,
|
||||
displayName: name,
|
||||
category: "single_phase",
|
||||
connectionKind: row.connection ? connectionLabels[row.connection] : undefined,
|
||||
costGroup: group.costGroup || undefined,
|
||||
quantity: row.unit === "area" ? null : 1,
|
||||
powerPerUnit: row.unit === "area" ? null : row.powerPerUnitW / 1000,
|
||||
simultaneityFactor: row.simultaneityFactor,
|
||||
cosPhi: row.cosPhi,
|
||||
remark: row.remark ?? undefined,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue