Make a partly-undone import retryable, and refuse a plan it cannot deliver

This commit is contained in:
2026-09-03 09:16:01 -04:00
parent b0bd73f62d
commit ddf41c3e65
20 changed files with 588 additions and 37 deletions

View File

@@ -1323,13 +1323,33 @@ td.actions .btn + .btn {
gap: 0.75rem;
margin-top: 0.4rem;
}
/* A branding preview has to show a logo whose colour we do not control. The
shipped GE mark is fill="white" because it lives on the dark sidebar, so on a
plain white tile it rendered white-on-white and read as a failed load. A
checker ground makes any logo visible - light, dark or transparent - and is
the usual convention for previewing artwork with alpha. */
.map-thumb {
height: 40px;
min-width: 40px;
border: 1px solid var(--border);
border-radius: 4px;
background: #fff;
/* MID-TONE, not white and not near-white. The tile has to show artwork whose
colour we do not control: the shipped GE mark is fill="white" for the dark
sidebar, the monogram is fill="black". A light ground hides the first, a
dark ground hides the second, so the checker sits either side of mid grey
and both read against it. */
background-color: #9aa1ad;
background-image:
linear-gradient(45deg, #6f7783 25%, transparent 25%, transparent 75%, #6f7783 75%),
linear-gradient(45deg, #6f7783 25%, transparent 25%, transparent 75%, #6f7783 75%);
background-size: 12px 12px;
background-position: 0 0, 6px 6px;
padding: 2px;
}
.map-thumb-dark {
background-color: #222;
background-image: none;
}
.map-thumb-dark { background: #222; }
.settings-success {
margin-top: 1rem;
padding: 0.75rem;

View File

@@ -24,7 +24,7 @@
>
<div class="map-upload-row">
<input type="file" :accept="logo.accept" @change="uploadLogo(logo.kind, logo.key, $event)" :disabled="brandingUploading" />
<img v-if="settings[logo.key]" :src="settings[logo.key]" class="map-thumb" :alt="logo.label" />
<img v-if="settings[logo.key]" :src="withBase(settings[logo.key])" class="map-thumb" :alt="logo.label" />
</div>
<small class="input-hint">{{ logo.hint }}</small>
</label>
@@ -132,6 +132,12 @@
<script setup>
import { onMounted } from 'vue'
import { useSystemSettings, brandingLogos } from '../../composables/systemSettings'
// The stored value is root-relative ('/ge-aerospace-logo.svg'). This app is
// mounted under a base path at every real deployment (/shopdb, /ops), so a raw
// src resolves against the SERVER root and 404s - which is what "error loading
// image" was. siteSettings.js already wraps every logo this way for the rest of
// the app; this preview was the one place that did not.
import { withBase } from '@/utils/basePath'
const {
settings, saving, brandingUploading, error, success,

View File

@@ -26,6 +26,14 @@
<button class="btn btn-small" @click="startAddLevel(building)">Add level</button>
</div>
<!-- Scroll rather than squeeze: nine columns of inputs do not fit a
narrow window, and without a wrapper the table ran off the right of
the page with the cells crushed out of shape.
NOT the global .table-container - that also sets white-space: nowrap,
and the sizing hint below is a full sentence, so nowrap made the
table WIDER instead of letting the paragraph wrap. Scroll here, wrap
inside the cells. -->
<div class="level-table-scroll">
<table class="data-table">
<thead>
<tr>
@@ -46,7 +54,7 @@
<td>
<input
v-model="level.levelname"
class="form-control"
class="form-control level-name-input"
@blur="renameLevel(level)"
:disabled="saving"
/>
@@ -123,6 +131,7 @@
</tr>
</tbody>
</table>
</div>
</div>
<!-- Add building -->
@@ -437,6 +446,17 @@ async function remove(level) {
}
.mono { font-family: monospace; }
/* Wide enough that every column keeps a usable width; past this the wrapper
scrolls instead of the inputs collapsing to a few pixels. */
.level-table-scroll { overflow-x: auto; }
.data-table { min-width: 58rem; }
/* The sizing note is prose and must wrap; it is the reason the shared
table-container wrapper is wrong for this table. */
.data-table .input-hint { white-space: normal; display: block; max-width: 22rem; }
.data-table td, .data-table th { vertical-align: top; }
/* A bare form-control in a table cell collapses to whatever width is left over,
which put "Ground floor" in a box one character wide. */
.level-name-input { width: 11rem; }
.order-input { width: 4.5rem; }
.size-input { width: 6rem; }
.size-fields { display: flex; align-items: center; gap: 0.35rem; }

View File

@@ -269,7 +269,7 @@
below if needed.
</template>
<template v-else-if="wincReport.blocking">
{{ wincReport.rowcount }} row(s) in scope, but unresolved values
{{ wincReport.rowcount }} row(s) in scope, but the problems below
block the import - nothing was written. Resolve them and preview
again.
</template>
@@ -314,6 +314,29 @@
</label>
</div>
<!-- Blocking and NOT acknowledgeable, unlike needscreate above.
The packet blocks on these, so the page must name them or the
banner points at nothing the admin can see. -->
<div
v-if="wincReport.uncreatable && Object.keys(wincReport.uncreatable).length"
class="problems"
>
<h4>Rows this import cannot create (blocking)</h4>
<p class="muted">
The export needs these, but this import only ever creates business
units. Create them in Settings first, or alias the export values
to rows that already exist, then preview again.
</p>
<ul>
<li v-for="(items, group) in wincReport.uncreatable" :key="group">
<strong>{{ group }}:</strong>
<span v-for="(count, name) in items" :key="name">
"{{ name }}" ({{ count }}){{ ' ' }}
</span>
</li>
</ul>
</div>
<div v-if="wincReport.unresolved && Object.keys(wincReport.unresolved).length" class="problems">
<h4>Unresolved values (blocking)</h4>
<p class="muted">
@@ -354,15 +377,27 @@
<td>{{ b.createdat }}</td>
<td>{{ b.createdby || '-' }}</td>
<td>{{ b.rowcount }}</td>
<td>{{ b.status }}</td>
<td>
{{ b.status }}
<small v-if="b.status === 'partially-undone'" class="muted">
- some rows could not be put back
</small>
</td>
<td>
<!-- 'partially-undone' is retryable and MUST offer the
button: that status exists because an undo stopped at a
conflict, and retrying once the conflict is resolved is
the whole point. Testing only for 'applied' left the
backend willing and the button hidden. -->
<button
v-if="b.status === 'applied'"
v-if="b.status === 'applied' || b.status === 'partially-undone'"
class="btn btn-secondary btn-sm"
:disabled="wincUndoingId === b.batchid"
@click="undoWincBatch(b.batchid)"
>
{{ wincUndoingId === b.batchid ? 'Undoing...' : 'Undo' }}
{{ wincUndoingId === b.batchid
? 'Undoing...'
: (b.status === 'partially-undone' ? 'Retry undo' : 'Undo') }}
</button>
<span v-else class="muted">undone</span>
</td>