Add estimate row project device handoff
This commit is contained in:
parent
8b1923e8ec
commit
a6500e2b26
9 changed files with 434 additions and 21 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue