Reworked Layout

This commit is contained in:
simon 2026-05-26 17:40:37 +02:00
parent bca4fcc936
commit aeeefc89c1

View File

@ -15,6 +15,7 @@
--accent: #3b82f6; --accent: #3b82f6;
--ok: #22c55e; --ok: #22c55e;
--err: #ef4444; --err: #ef4444;
--sidebar-w: 260px;
} }
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
body { body {
@ -24,40 +25,135 @@
min-height: 100vh; min-height: 100vh;
line-height: 1.5; line-height: 1.5;
} }
.page { .app {
max-width: 1000px; display: flex;
margin: 0 auto; min-height: 100vh;
padding: 2rem 1.5rem;
} }
header { .sidebar {
margin-bottom: 2rem; width: var(--sidebar-w);
padding-bottom: 1.5rem; flex-shrink: 0;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
}
.sidebar-brand {
padding: 1.25rem 1rem 1rem;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
header h1 { font-size: 1.5rem; font-weight: 600; } .sidebar-brand h1 { font-size: 1.1rem; font-weight: 600; }
header p { color: var(--muted); margin-top: 0.25rem; font-size: 0.9rem; } .sidebar-brand p { color: var(--muted); font-size: 0.8rem; margin-top: 0.2rem; }
.grid { .sidebar-nav {
display: grid; flex: 1;
gap: 1rem; overflow-y: auto;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); padding: 0.5rem 0;
} }
.card { .nav-section { margin-bottom: 0.25rem; }
.nav-section-header {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.55rem 1rem;
background: none;
border: none;
color: var(--text);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
text-align: left;
}
.nav-section-header:hover { background: rgba(255,255,255,0.04); }
.nav-section-header.active { color: var(--accent); }
.nav-chevron {
width: 0.65rem;
height: 0.65rem;
border-right: 2px solid var(--muted);
border-bottom: 2px solid var(--muted);
transform: rotate(-45deg);
transition: transform 0.15s;
flex-shrink: 0;
margin-left: auto;
}
.nav-chevron.open { transform: rotate(45deg); margin-top: -0.2rem; }
.nav-children {
padding: 0.15rem 0 0.35rem;
}
.nav-item {
display: block;
width: 100%;
padding: 0.4rem 1rem 0.4rem 1.75rem;
background: none;
border: none;
color: var(--muted);
font-size: 0.82rem;
text-align: left;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-item:hover { color: var(--text); background: rgba(255,255,255,0.04); }
.nav-item.selected {
color: var(--text);
background: rgba(59, 130, 246, 0.15);
border-right: 2px solid var(--accent);
}
.nav-item.add {
color: var(--accent);
font-weight: 500;
}
.nav-item.add:hover { background: rgba(59, 130, 246, 0.1); }
.nav-empty {
padding: 0.25rem 1rem 0.25rem 1.75rem;
font-size: 0.78rem;
color: var(--muted);
font-style: italic;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid var(--border);
}
.sidebar-footer label { font-size: 0.75rem; color: var(--muted); display: block; margin-bottom: 0.35rem; }
.sidebar-footer input {
width: 100%;
padding: 0.4rem 0.6rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.78rem;
}
.api-status { font-size: 0.75rem; margin-top: 0.35rem; }
.api-status.ok { color: var(--ok); }
.api-status.err { color: var(--err); }
.main {
flex: 1;
min-width: 0;
padding: 2rem 2rem 3rem;
max-width: 900px;
}
.main-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.main-header h2 { font-size: 1.35rem; font-weight: 600; }
.main-header p { color: var(--muted); margin-top: 0.25rem; font-size: 0.9rem; }
.panel {
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
padding: 1.25rem; padding: 1.5rem;
}
.card.wide { grid-column: 1 / -1; }
.card h2 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 0.75rem;
} }
.muted { color: var(--muted); font-size: 0.85rem; margin-top: 0.5rem; } .muted { color: var(--muted); font-size: 0.85rem; margin-top: 0.5rem; }
button { button {
margin-top: 1rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
background: var(--accent); background: var(--accent);
color: #fff; color: #fff;
@ -74,17 +170,16 @@
color: var(--accent); color: var(--accent);
} }
button.secondary:hover { background: rgba(59, 130, 246, 0.12); filter: none; } button.secondary:hover { background: rgba(59, 130, 246, 0.12); filter: none; }
.btn-row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem; } .btn-row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1.25rem; align-items: center; }
button.danger { button.danger {
background: transparent; background: transparent;
border: 1px solid var(--err); border: 1px solid var(--err);
color: var(--err); color: var(--err);
margin-top: 0; padding: 0.5rem 1rem;
padding: 0.35rem 0.65rem; font-size: 0.875rem;
font-size: 0.8rem;
} }
button.danger:hover { background: rgba(239, 68, 68, 0.12); filter: none; } button.danger:hover { background: rgba(239, 68, 68, 0.12); filter: none; }
input { input, select {
width: 100%; width: 100%;
margin-top: 0.5rem; margin-top: 0.5rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@ -101,65 +196,28 @@
gap: 0.5rem 1rem; gap: 0.5rem 1rem;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
} }
table { .msg-err { color: var(--err); font-size: 0.85rem; margin-top: 0.75rem; }
width: 100%; .msg-ok { color: var(--ok); font-size: 0.85rem; margin-top: 0.75rem; }
border-collapse: collapse; .item-preview-panel {
font-size: 0.85rem; margin-bottom: 1.25rem;
margin-top: 0.75rem;
}
th, td {
text-align: left;
padding: 0.5rem 0.25rem;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
th { color: var(--muted); font-weight: 500; font-size: 0.75rem; }
.empty { color: var(--muted); font-size: 0.85rem; margin-top: 0.5rem; }
.msg-err { color: var(--err); font-size: 0.85rem; margin-top: 0.5rem; }
.msg-ok { color: var(--ok); font-size: 0.85rem; margin-top: 0.5rem; }
.item-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
margin-top: 1rem;
}
.item-card {
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
overflow: hidden; padding: 1rem;
display: flex;
flex-direction: column;
}
.item-preview {
background: #fff;
padding: 0.75rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 160px; min-height: 180px;
} }
.item-preview img { .item-preview-panel img {
max-width: 100%; max-width: 100%;
max-height: 140px; max-height: 160px;
width: auto; background: #fff;
height: auto; padding: 0.5rem;
border-radius: 4px;
} }
.item-body {
padding: 0.75rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.item-body strong { font-size: 0.9rem; }
.item-body span { font-size: 0.8rem; color: var(--muted); }
.item-actions {
padding: 0 0.75rem 0.75rem;
}
.row-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }
.layout-preview-wrap { .layout-preview-wrap {
margin-top: 1rem; margin-top: 1.25rem;
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
@ -181,60 +239,119 @@
} }
.layout-stats strong { color: var(--text); } .layout-stats strong { color: var(--text); }
.layout-stats span { color: var(--muted); } .layout-stats span { color: var(--muted); }
select { .empty-state {
width: 100%; color: var(--muted);
margin-top: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.875rem;
}
.config-list {
display: grid;
gap: 1rem;
margin-top: 1rem;
}
.config-entry {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
}
.config-entry header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 0;
padding-bottom: 0.5rem;
border-bottom: none;
}
.config-entry header h3 {
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; padding: 2rem 0;
text-align: center;
}
@media (max-width: 720px) {
.app { flex-direction: column; }
.sidebar {
width: 100%;
height: auto;
position: relative;
max-height: 45vh;
}
.main { padding: 1.25rem; }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="page" x-data="dashboard()" x-init="init()"> <div class="app" x-data="dashboard()" x-init="init()">
<header> <aside class="sidebar">
<div class="sidebar-brand">
<h1>Printer Backend</h1> <h1>Printer Backend</h1>
<p>Platten und Items verwalten</p> <p>Admin</p>
</header> </div>
<div class="grid"> <nav class="sidebar-nav">
<section class="card"> <div class="nav-section">
<h2>API</h2> <button type="button" class="nav-section-header"
<label for="api-base">Basis-URL</label> :class="{ active: section === 'plates' }"
@click="openSection('plates')">
Platten
<span class="nav-chevron" :class="{ open: expanded.plates }"></span>
</button>
<div class="nav-children" x-show="expanded.plates">
<template x-if="plates.length === 0 && !platesLoading">
<p class="nav-empty">Keine Platten</p>
</template>
<template x-for="p in plates" :key="p.id">
<button type="button" class="nav-item"
:class="{ selected: section === 'plates' && plateEditingId === p.id }"
@click="selectPlate(p)"
x-text="sidebarPlateLabel(p)"></button>
</template>
<button type="button" class="nav-item add"
:class="{ selected: section === 'plates' && !plateEditingId }"
@click="newPlate()">+ Neu</button>
</div>
</div>
<div class="nav-section">
<button type="button" class="nav-section-header"
:class="{ active: section === 'items' }"
@click="openSection('items')">
Items
<span class="nav-chevron" :class="{ open: expanded.items }"></span>
</button>
<div class="nav-children" x-show="expanded.items">
<template x-if="items.length === 0 && !itemsLoading">
<p class="nav-empty">Keine Items</p>
</template>
<template x-for="it in items" :key="it.id">
<button type="button" class="nav-item"
:class="{ selected: section === 'items' && itemEditingId === it.id }"
@click="selectItem(it)"
x-text="labelItem(it)"></button>
</template>
<button type="button" class="nav-item add"
:class="{ selected: section === 'items' && !itemEditingId }"
@click="newItem()">+ Neu</button>
</div>
</div>
<div class="nav-section">
<button type="button" class="nav-section-header"
:class="{ active: section === 'configurations' }"
@click="openSection('configurations')">
Konfigurationen
<span class="nav-chevron" :class="{ open: expanded.configurations }"></span>
</button>
<div class="nav-children" x-show="expanded.configurations">
<template x-if="configurations.length === 0">
<p class="nav-empty">Keine Konfigurationen</p>
</template>
<template x-for="c in configurations" :key="c.id">
<button type="button" class="nav-item"
:class="{ selected: section === 'configurations' && configEditingId === c.id }"
@click="selectConfiguration(c)"
x-text="sidebarConfigLabel(c)"></button>
</template>
<button type="button" class="nav-item add"
:class="{ selected: section === 'configurations' && !configEditingId }"
@click="newConfiguration()">+ Neu</button>
</div>
</div>
</nav>
<div class="sidebar-footer">
<label for="api-base">API</label>
<input id="api-base" type="url" x-model="apiBase" @change="onApiBaseChange()" placeholder="http://127.0.0.1:8080"> <input id="api-base" type="url" x-model="apiBase" @change="onApiBaseChange()" placeholder="http://127.0.0.1:8080">
<p class="muted" x-show="apiOk === true">API erreichbar</p> <p class="api-status ok" x-show="apiOk === true">Erreichbar</p>
<p class="msg-err" x-show="apiOk === false">API nicht erreichbar</p> <p class="api-status err" x-show="apiOk === false">Nicht erreichbar</p>
</section> </div>
</aside>
<section class="card wide"> <main class="main">
<h2 x-text="plateEditingId ? 'Platte bearbeiten' : 'Platte anlegen'"></h2> <!-- Platten -->
<div x-show="section === 'plates'">
<div class="main-header">
<h2 x-text="plateEditingId ? 'Platte bearbeiten' : 'Neue Platte'"></h2>
<p class="muted" x-show="!plateEditingId">Druckbett mit Abmessungen und Rändern anlegen.</p>
</div>
<div class="panel">
<form @submit.prevent="savePlate()"> <form @submit.prevent="savePlate()">
<div class="form-grid"> <div class="form-grid">
<div> <div>
@ -268,51 +385,27 @@
</div> </div>
<div class="btn-row"> <div class="btn-row">
<button type="submit" :disabled="plateSaving"> <button type="submit" :disabled="plateSaving">
<span x-text="plateSaving ? 'Speichere…' : (plateEditingId ? 'Änderungen speichern' : 'Platte anlegen')"></span> <span x-text="plateSaving ? 'Speichere…' : (plateEditingId ? 'Speichern' : 'Anlegen')"></span>
</button> </button>
<button type="button" class="secondary" x-show="plateEditingId" @click="cancelPlateEdit()">Abbrechen</button> <button type="button" class="danger" x-show="plateEditingId" @click="deletePlate(plateEditingId)">Löschen</button>
</div> </div>
<p class="msg-err" x-show="plateError" x-text="plateError"></p> <p class="msg-err" x-show="plateError" x-text="plateError"></p>
<p class="msg-ok" x-show="plateSuccess" x-text="plateSuccess"></p> <p class="msg-ok" x-show="plateSuccess" x-text="plateSuccess"></p>
</form> </form>
</section> </div>
</div>
<section class="card wide"> <!-- Items -->
<h2>Platten</h2> <div x-show="section === 'items'">
<template x-if="plates.length === 0 && !platesLoading"> <div class="main-header">
<p class="empty">Keine Platten.</p> <h2 x-text="itemEditingId ? 'Item bearbeiten' : 'Neues Item'"></h2>
</template>
<table x-show="plates.length > 0">
<thead>
<tr>
<th>Name</th>
<th>Größe</th>
<th>Margins</th>
<th>Druckfläche</th>
<th></th>
</tr>
</thead>
<tbody>
<template x-for="p in plates" :key="p.id">
<tr>
<td x-text="p.name || '—'"></td>
<td x-text="fmtSize(p.width_mm, p.height_mm)"></td>
<td x-text="fmtMargins(p)"></td>
<td x-text="fmtPrintable(p)"></td>
<td class="row-actions">
<button type="button" class="secondary" @click="editPlate(p)">Bearbeiten</button>
<button type="button" class="danger" @click="deletePlate(p.id)">Löschen</button>
</td>
</tr>
</template>
</tbody>
</table>
</section>
<section class="card wide">
<h2 x-text="itemEditingId ? 'Item bearbeiten' : 'Item anlegen'"></h2>
<p class="muted" x-show="!itemEditingId">Erzeugt SVG-Maske in <code>data/svg_template/</code>.</p> <p class="muted" x-show="!itemEditingId">Erzeugt SVG-Maske in <code>data/svg_template/</code>.</p>
<p class="muted" x-show="itemEditingId">Aktualisiert SVG und Metadaten; Dateiname bleibt unverändert.</p> <p class="muted" x-show="itemEditingId">SVG und Metadaten aktualisieren; Dateiname bleibt unverändert.</p>
</div>
<div class="panel">
<div class="item-preview-panel" x-show="itemEditingId">
<img :src="itemSvgUrl(itemEditingId)" alt="SVG-Vorschau" loading="lazy">
</div>
<form @submit.prevent="saveItem()"> <form @submit.prevent="saveItem()">
<div class="form-grid"> <div class="form-grid">
<div> <div>
@ -346,18 +439,23 @@
</div> </div>
<div class="btn-row"> <div class="btn-row">
<button type="submit" :disabled="itemSaving"> <button type="submit" :disabled="itemSaving">
<span x-text="itemSaving ? 'Speichere…' : (itemEditingId ? 'Änderungen speichern' : 'Item anlegen')"></span> <span x-text="itemSaving ? 'Speichere…' : (itemEditingId ? 'Speichern' : 'Anlegen')"></span>
</button> </button>
<button type="button" class="secondary" x-show="itemEditingId" @click="cancelItemEdit()">Abbrechen</button> <button type="button" class="danger" x-show="itemEditingId" @click="deleteItem(itemEditingId)">Löschen</button>
</div> </div>
<p class="msg-err" x-show="itemError" x-text="itemError"></p> <p class="msg-err" x-show="itemError" x-text="itemError"></p>
<p class="msg-ok" x-show="itemSuccess" x-text="itemSuccess"></p> <p class="msg-ok" x-show="itemSuccess" x-text="itemSuccess"></p>
</form> </form>
</section> </div>
</div>
<section class="card wide"> <!-- Konfigurationen -->
<h2 x-text="configEditingId ? 'Konfiguration bearbeiten' : 'Konfiguration — Layout-Vorschau'"></h2> <div x-show="section === 'configurations'">
<p class="muted">Kombiniert Platte und Item; berechnet maximale Stückzahl unter Einhaltung aller Margins und Abstände.</p> <div class="main-header">
<h2 x-text="configEditingId ? 'Konfiguration bearbeiten' : 'Neue Konfiguration'"></h2>
<p class="muted">Kombiniert Platte und Item; berechnet maximale Stückzahl.</p>
</div>
<div class="panel">
<form @submit.prevent="saveConfiguration()"> <form @submit.prevent="saveConfiguration()">
<div class="form-grid"> <div class="form-grid">
<div> <div>
@ -389,13 +487,13 @@
</div> </div>
<div class="btn-row"> <div class="btn-row">
<button type="submit" :disabled="configSaving || !configForm.plate_id || !configForm.item_id"> <button type="submit" :disabled="configSaving || !configForm.plate_id || !configForm.item_id">
<span x-text="configSaving ? 'Speichere…' : (configEditingId ? 'Änderungen speichern' : 'Konfiguration speichern')"></span> <span x-text="configSaving ? 'Speichere…' : (configEditingId ? 'Speichern' : 'Anlegen')"></span>
</button> </button>
<button type="button" class="secondary" x-show="configEditingId" @click="cancelConfigEdit()">Abbrechen</button>
<button type="button" class="secondary" @click="downloadLayoutPDF()" <button type="button" class="secondary" @click="downloadLayoutPDF()"
:disabled="pdfGenerating || !layoutPreview || !configForm.plate_id || !configForm.item_id"> :disabled="pdfGenerating || !layoutPreview || !configForm.plate_id || !configForm.item_id">
<span x-text="pdfGenerating ? 'PDF…' : 'PDF-Vorschau'"></span> <span x-text="pdfGenerating ? 'PDF…' : 'PDF-Vorschau'"></span>
</button> </button>
<button type="button" class="danger" x-show="configEditingId" @click="deleteConfiguration(configEditingId)">Löschen</button>
</div> </div>
<p class="msg-err" x-show="configError" x-text="configError"></p> <p class="msg-err" x-show="configError" x-text="configError"></p>
<p class="msg-ok" x-show="configSuccess" x-text="configSuccess"></p> <p class="msg-ok" x-show="configSuccess" x-text="configSuccess"></p>
@ -413,69 +511,13 @@
</div> </div>
</div> </div>
</template> </template>
<p class="empty" x-show="!layoutPreview && configForm.plate_id && configForm.item_id && !layoutLoading"> <p class="muted" x-show="!layoutPreview && configForm.plate_id && configForm.item_id && !layoutLoading">
Keine Vorschau (Platte zu klein oder API-Fehler). Keine Vorschau (Platte zu klein oder API-Fehler).
</p> </p>
<p class="muted" x-show="layoutLoading">Berechne Layout…</p> <p class="muted" x-show="layoutLoading">Berechne Layout…</p>
</section>
<section class="card wide" x-show="configurations.length > 0">
<h2>Gespeicherte Konfigurationen</h2>
<div class="config-list">
<template x-for="c in configurations" :key="c.id">
<article class="config-entry">
<header>
<div>
<h3 x-text="c.name || ('Konfiguration ' + c.id.slice(0, 8))"></h3>
<p class="muted" x-text="configSummary(c)"></p>
</div>
<div class="row-actions">
<button type="button" class="secondary" @click="editConfiguration(c)">Bearbeiten</button>
<button type="button" class="secondary" @click="downloadConfigurationPDF(c.id)"
:disabled="pdfGenerating || !!c.preview_error" x-show="c.preview && !c.preview_error">
PDF
</button>
<button type="button" class="danger" @click="deleteConfiguration(c.id)">Löschen</button>
</div>
</header>
<p class="msg-err" x-show="c.preview_error" x-text="c.preview_error"></p>
<div class="layout-preview-wrap" x-show="c.preview && !c.preview_error">
<div x-html="layoutPreviewSvg(c.preview)"></div>
<div class="layout-stats">
<span><strong x-text="c.preview.count"></strong> Items</span>
<span><strong x-text="c.preview.columns + ' × ' + c.preview.rows"></strong></span>
</div> </div>
</div> </div>
</article> </main>
</template>
</div>
</section>
<section class="card wide">
<h2>Items</h2>
<template x-if="items.length === 0 && !itemsLoading">
<p class="empty">Keine Items.</p>
</template>
<div class="item-grid" x-show="items.length > 0">
<template x-for="it in items" :key="it.id">
<article class="item-card">
<div class="item-preview">
<img :src="itemSvgUrl(it.id)" :alt="labelItem(it)" loading="lazy">
</div>
<div class="item-body">
<strong x-text="labelItem(it)"></strong>
<span x-text="itemSizeLabel(it)"></span>
<span><code x-text="it.svg_template"></code></span>
</div>
<div class="item-actions row-actions">
<button type="button" class="secondary" @click="editItem(it)">Bearbeiten</button>
<button type="button" class="danger" @click="deleteItem(it.id)">Löschen</button>
</div>
</article>
</template>
</div>
</section>
</div>
</div> </div>
<script> <script>
@ -484,6 +526,9 @@
apiBase: '', apiBase: '',
apiOk: null, apiOk: null,
section: 'plates',
expanded: { plates: true, items: false, configurations: false },
plates: [], plates: [],
platesLoading: false, platesLoading: false,
plateSaving: false, plateSaving: false,
@ -532,6 +577,60 @@
pdfGenerating: false, pdfGenerating: false,
_layoutTimer: null, _layoutTimer: null,
openSection(name) {
if (this.section === name) {
this.expanded[name] = !this.expanded[name];
} else {
this.section = name;
this.expanded[name] = true;
}
},
selectPlate(p) {
this.section = 'plates';
this.expanded.plates = true;
this.editPlate(p);
},
newPlate() {
this.section = 'plates';
this.expanded.plates = true;
this.cancelPlateEdit();
},
selectItem(it) {
this.section = 'items';
this.expanded.items = true;
this.editItem(it);
},
newItem() {
this.section = 'items';
this.expanded.items = true;
this.cancelItemEdit();
},
selectConfiguration(c) {
this.section = 'configurations';
this.expanded.configurations = true;
this.editConfiguration(c);
},
newConfiguration() {
this.section = 'configurations';
this.expanded.configurations = true;
this.cancelConfigEdit();
},
sidebarPlateLabel(p) {
const n = p.name || 'Platte';
return n + ' · ' + this.fmtSize(p.width_mm, p.height_mm);
},
sidebarConfigLabel(c) {
return c.name || ('Konfiguration ' + c.id.slice(0, 8));
},
apiUrl(path) { apiUrl(path) {
return `${this.apiBase.replace(/\/$/, '')}${path}`; return `${this.apiBase.replace(/\/$/, '')}${path}`;
}, },
@ -655,8 +754,15 @@
await this.apiJSON('POST', '/plates', body); await this.apiJSON('POST', '/plates', body);
this.plateSuccess = 'Platte gespeichert.'; this.plateSuccess = 'Platte gespeichert.';
} }
this.cancelPlateEdit(); const keepId = this.plateEditingId;
await this.loadPlates(); await this.loadPlates();
if (keepId) {
const p = this.plates.find(x => x.id === keepId);
if (p) this.editPlate(p);
else this.cancelPlateEdit();
} else {
this.cancelPlateEdit();
}
this.refreshLayoutPreview(); this.refreshLayoutPreview();
} catch (e) { } catch (e) {
this.plateError = String(e.message || e); this.plateError = String(e.message || e);
@ -799,8 +905,15 @@
await this.apiJSON('POST', '/items', body); await this.apiJSON('POST', '/items', body);
this.itemSuccess = 'Item erzeugt.'; this.itemSuccess = 'Item erzeugt.';
} }
this.cancelItemEdit(); const keepId = this.itemEditingId;
await this.loadItems(); await this.loadItems();
if (keepId) {
const it = this.items.find(x => x.id === keepId);
if (it) this.editItem(it);
else this.cancelItemEdit();
} else {
this.cancelItemEdit();
}
this.refreshLayoutPreview(); this.refreshLayoutPreview();
} catch (e) { } catch (e) {
this.itemError = String(e.message || e); this.itemError = String(e.message || e);
@ -885,8 +998,15 @@
await this.apiJSON('POST', '/configurations', body); await this.apiJSON('POST', '/configurations', body);
this.configSuccess = 'Konfiguration gespeichert.'; this.configSuccess = 'Konfiguration gespeichert.';
} }
this.cancelConfigEdit(); const keepId = this.configEditingId;
await this.loadConfigurations(); await this.loadConfigurations();
if (keepId) {
const c = this.configurations.find(x => x.id === keepId);
if (c) this.editConfiguration(c);
else this.cancelConfigEdit();
} else {
this.cancelConfigEdit();
}
} catch (e) { } catch (e) {
this.configError = String(e.message || e); this.configError = String(e.message || e);
} finally { } finally {
@ -927,11 +1047,13 @@
item_id: this.configForm.item_id, item_id: this.configForm.item_id,
spacing_mm: String(Number(this.configForm.spacing_mm) || 0), spacing_mm: String(Number(this.configForm.spacing_mm) || 0),
}); });
this.downloadBlob('/layout/pdf?' + q, 'layout-preview.pdf'); const name = this.configEditingId
}, ? 'configuration-' + this.configEditingId.slice(0, 8) + '.pdf'
: 'layout-preview.pdf';
downloadConfigurationPDF(id) { const path = this.configEditingId
this.downloadBlob('/configurations/' + id + '/pdf', 'configuration-' + id.slice(0, 8) + '.pdf'); ? '/configurations/' + this.configEditingId + '/pdf'
: '/layout/pdf?' + q;
this.downloadBlob(path, name);
}, },
async deleteConfiguration(id) { async deleteConfiguration(id) {